React Slot Pattern で Render Props を省略する
Radix UIのSlotコンポーネントが興味深かったので記事にしました。
Radix UI について
Radix UIはheadless UIライブラリで、スタイルを提供するのではなく、コンポーネントの機能を提供するライブラリです。Radix UIの雰囲気は以下の記事が参考になります。
例としてSwitchコンポーネントは以下のようなインターフェースになっています。
このように小さなコンポーネントを集約して1つの機能を表す仕組みになっています。
import * as Switch from '@radix-ui/react-switch';
export default () => (
<Switch.Root>
<Switch.Thumb />
</Switch.Root>
);
Slot コンポーネント
Radix UIのベースであるPrimitiveコンポーネントの1つにSlotコンポーネントがあります。
Slotコンポーネントを使うことで親コンポーネントにある処理を子コンポーネントの中で走らせることができます。
Slotを使った具体例をあげます。
import { Slot } from "@radix-ui/react-slot";
function Anchor({ props }) {
return <Slot {...props} />;
}
export default function App() {
return (
<div className="App">
<Anchor href="/about">
<a>Link</a>
</Anchor>
</div>
);
}
Anchor
のhref
propsにURLに渡していますが、実際のApp
の描画結果は以下のようになり、a
タグにhref
が渡されます。
<div class="App">
<a href="/about">Link</a>
</div>
これだけだと何が嬉しいのかよくわからないですね。
なので、もう少し具体的な例を出してみます。
e.g. Link コンポーネント
先ほど定義したAnchor
コンポーネント、どこかで見覚えがないでしょうか?
Next.js の Link コンポーネントで同様のパターンを使っています。
import Link from 'next/link';
export default function App() {
return (
<div className="App">
<Link href="/about">
<a>Link</a>
</Link>
</div>
);
}
Link
コンポーネントも先ほどのAnchor
コンポーネントと同様で、a
tagにhref
が渡ります。さらに、リンクをクリックした時には、Link
コンポーネントで実装されているonClick
のcallbackが呼ばれます。また、それ以外にもprefetchの処理など、Link
コンポーネントの中には様々な処理が入っています。
Slotコンポーネント、または同様のパターンを使うことで、処理したいものを隠蔽しつつ、実際のPropsのイベントハンドリングやスタイリングは自分達で定義した子のコンポーネントで行わせることができます。
Render Props Pattern
似たような、ロジックを隠蔽するパターンとして、 Render Propsパターンがありました。このパターンは、描画に関わるPropsを子に渡すことで、ロジックを再利用可能な形に分割できます。
Render Propsパターンを使って、先ほどの Anchor
コンポーネントを実装してみましょう。
function Anchor({ children, ...props }) {
return children(props);
}
export default function App() {
return (
<div className="App">
<Anchor href="/about">
{(props) => <a {...props}>Link</a>}
</Anchor>
</div>
);
}
ここで、Anchor
の中をSlotパターンと見比べてみましょう。
// Render props pattern
<Anchor href="/about">
{(props) => <a {...props}>Link</a>}
</Anchor>
// Slot pattern
<Anchor href="/about">
<a>Link</a>
</Anchor>
見比べるとわかるように、Slot
コンポーネントは、Render Propsパターンの省略形になっています。 言い換えれば、Children
を1つしか持たないようなコンポーネントは、Slot
を使うことで、{() => <div />}
といった特殊なChildren
を要求しないシンプルな形に置き換えられます。
つまり、単機能なコンポーネントとSlot
は相性が良いと言えそうです。
さて、Radix UIは「小さなコンポーネントを集約して1つの機能を表す仕組み」だと最初に述べました。ここでもう一度Radix UIのSwitchを見てみましょう。
import * as Switch from '@radix-ui/react-switch';
export default () => (
<Switch.Root>
<Switch.Thumb />
</Switch.Root>
);
実は、Radix UIは、すべてのコンポーネントはこのSlotパターンを asProps
として提供しています。これによって、なんとHTMLのセマンティック要素も分割することが可能になっています!
import * as Switch from '@radix-ui/react-switch';
export default () => (
<Switch.Root asChild>
<input>
<Switch.Thumb asChild>
<span />
</Switch.Thumb>
</input>
</Switch.Root>
);
まとめ
- Slotコンポーネントを使うことでロジックの隠蔽・分割ができる。
- Slotパターンは、Childrenを1つしか持たないRender Propsパターンの省略形である。
- UIライブラリなど汎用的かつ単機能なコンポーネントとの相性が良さそう
Discussion