React TableにみられるHeadless UIなライブラリ
-
最近 react-table を使う機会があったので README を読んでいると、React Table is a "headless" UI library
という面白そうなコンセプトが書かれていました。
今回は react-table にみられる Headless UI
についての内容になります。
React Table
react-table は、ソートやフィルターなど機能が搭載された Table コンポーネントを提供してくれる React の UI ライブラリです。
現時点で最新バージョンである v7 から hook に対応した書き方へと大幅な変更が行われました。そして今回の主題である Headless UI は v7 以降に採用されています。
Why React Table is a "headless" UI library
UI ライブラリが Headless になることは、どんなメリットがあるのでしょうか。react-table の doc によると、その答えは「Headless User Interface Components」に書かれているとのことなので、読んでみます。
あなたは「Heads」「Tails」がそれぞれ 50% の確率で現れるコイントスを実装する必要があるとする。マネージャーに「これは何年もの研究が必要だ!」と言って早速仕事に取り掛かる。
const CoinFlip = () =>
Math.random() < 0.5 ? <div>Heads</div> : <div>Tails</div>;
コイントスの実装は思ったより簡単だったため、誇らしげにすぐ成果を共通した。
「素晴らしい! ついでにクールな画像も表示できますか?」「もちろんだ!」
const CoinFlip = () =>
Math.random() < 0.5 ? (
<div>
<img src="/heads.svg" alt="Heads" />
</div>
) : (
<div>
<img src="/tails.svg" alt="Tails" />
</div>
);
「SEO のために『Heads』と『Tails』のラベルも欲しいです」「オーケー(マーケティングサイトにでも使うのか?)」
const CoinFlip = (
// We'll default to false to avoid breaking the applications
// current usage.
{ showLabels = false }
) =>
Math.random() < 0.5 ? (
<div>
<img src="/heads.svg" alt="Heads" />
{/* Add these labels for the marketing site. */}
{showLabels && <span>Heads</span>}
</div>
) : (
<div>
<img src="/tails.svg" alt="Tails" />
{/* Add these labels for the marketing site. */}
{showLabels && <span>Tails</span>}
</div>
);
その後すぐに新たな要件が出現する。
「クリックすると確率が変わるボタンを追加できますか?」「………」
const flip = () => ({
flipResults: Math.random()
});
class CoinFlip extends React.Component {
static defaultProps = {
showLabels: false,
// We don't repurpose `showLabels`, we aren't animals, after all.
showButton: false
};
state = flip();
handleClick = () => {
this.setState(flip);
};
render() {
return (
// Use fragments so people take me seriously.
<>
{this.state.showButton && (
<button onClick={this.handleClick}>Reflip</button>
)}
{this.state.flipResults < 0.5 ? (
<div>
<img src="/heads.svg" alt="Heads" />
{showLabels && <span>Heads</span>}
</div>
) : (
<div>
<img src="/tails.svg" alt="Tails" />
{showLabels && <span>Tails</span>}
</div>
)}
</>
);
}
}
最後に同僚から「確率を調整できるサイコロ機能をもった <DiceRoll /> を作りたいんだけど、君の <CoinFlip /> を再利用できない?」と。
あなたには 2 つの選択肢がある。1 つは崩壊しないように <DiceRoll />
の機能を取り込むこと。もう 1 つは「申し訳ありませんが、共有できるものはここにはございません」と返事すること。
Enter Headless Components
Headless な UI コンポーネントは view からロジックと動作を分離します。このパターンは、コンポーネントのロジックが十分に複雑で、view から切り離されている場合に非常に効果的です。
<CoinFlip/>
を Headless で実装すると次のようになります。
const flip = () => ({
flipResults: Math.random()
});
class CoinFlip extends React.Component {
state = flip();
handleClick = () => {
this.setState(flip);
};
render() {
return this.props.children({
rerun: this.handleClick,
isHeads: this.state.flipResults < 0.5
});
}
}
このコンポーネントは何もレンダリングしません。つまり headless です。
具体的なアプリケーションのコードは次のようになります。
<CoinFlip>
{({ rerun, isHeads }) => (
<>
<button onClick={rerun}>Reflip</button>
{isHeads ? (
<div>
<img src="/heads.svg" alt="Heads" />
</div>
) : (
<div>
<img src="/tails.svg" alt="Tails" />
</div>
)}
</>
)}
</CoinFlip>
マーケティングサイトではこう:
<CoinFlip>
{({ isHeads }) => (
<>
{isHeads ? (
<div>
<img src="/heads.svg" alt="Heads" />
<span>Heads</span>
</div>
) : (
<div>
<img src="/tails.svg" alt="Tails" />
<span>Tails</span>
</div>
)}
</>
)}
</CoinFlip>
プレゼンテーションからロジックを完全に分離できました! これにより、視覚的な柔軟性が大幅に向上します。
> でも、これって単なるレンダリングのいち手法じゃない?
そのとおり。この headless コンポーネントはレンダープロップとして実装されています。しかし状況に応じて HOC として実装することもできます。
ポイントは、コインを反転させる「メカニズム」と、そのメカニズムへの「インターフェース」を分離させることです。
What about <DiceRoll />?
Headless コンポーネントのすばらしい点は、コンポーネントを一般化して同僚の新しい <DiceRoll /> 機能をサポートすることが非常に簡単になるということです。
const run = () => ({
random: Math.random()
});
class Probability extends React.Component {
state = run();
handleClick = () => {
this.setState(run);
};
render() {
return this.props.children({
rerun: this.handleClick,
// By taking in a threshold property we can support
// different odds!
result: this.state.random < this.props.threshold
});
}
}
この headless コンポーネントを使用すると、コンシューマーに変更を加えることなく <CoinFlip /> の実装を変更できます。
const CoinFlip = ({ children }) => (
<Probability threshold={0.5}>
{({ rerun, result }) =>
children({
isHeads: result,
rerun
})
}
</Probability>
);
これにて無事に同僚は <RollDice /> を実装することができます。
const RollDice = ({ children }) => (
// Six Sided Dice
<Probability threshold={1 / 6}>
{({ rerun, result }) => (
<div>
{/* She was able to use a different event! */}
<span onMouseOver={rerun}>Roll the dice!</span>
{/* Totally different interface! */}
{result ? (
<div>Big winner!</div>
) : (
<div>You win some, you lose most.</div>
)}
</div>
)}
</Probability>
);
きれいでしょ?
Headless UI がもたらす3つのメリット
「Headless User Interface Components」の内容を理解したところで、react-table が Headless UI を採用するメリットは 3 つあると書かれています。
- ロジックと動作の分離
- メンテナンス
- 拡張性
1 と 3 は <CoinFlip /> の例で確認したとおりです。react-table を使うユーザーは見た目に専念でき、また拡張性も非常に高いものになることが容易に想像できます。また React Hook に対応した書き方のおかげで、よりシンプルでわかりやすい実装になっています。
2 はメンテナー側の視点です。ロジックの API だけを管理すればいいため、更新・メンテナンスが簡潔になります。結果的に利用者もライブラリが使いやすくなるので、双方ともに win-win ですね。
まとめ
Headless UI そのものは数年前からすでに存在していたみたいです。実際に「Headless User Interface Components」の筆者は downshift から影響を受けたと言っていました。
副作用などの独自の処理を hook に閉じ込めて作る React のカスタムフックですが、うまく使うことで headless UI なコンポーネントも作りやすそうです。