🎰

React Slot Pattern で Render Props を省略する

2022/02/21に公開

Radix UIのSlotコンポーネントが興味深かったので記事にしました。

Radix UI について

Radix UIはheadless UIライブラリで、スタイルを提供するのではなく、コンポーネントの機能を提供するライブラリです。Radix UIの雰囲気は以下の記事が参考になります。

https://zenn.dev/ynakamura/articles/d30ee1cb6f3a15

例としてSwitchコンポーネントは以下のようなインターフェースになっています。
このように小さなコンポーネントを集約して1つの機能を表す仕組みになっています。

import * as Switch from '@radix-ui/react-switch';

export default () => (
  <Switch.Root>
    <Switch.Thumb />
  </Switch.Root>
);

Slot コンポーネント

https://www.radix-ui.com/docs/primitives/utilities/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>
  );
}

Anchorhref propsにURLに渡していますが、実際のAppの描画結果は以下のようになり、aタグにhrefが渡されます。

<div class="App">
  <a href="/about">Link</a>
</div>

これだけだと何が嬉しいのかよくわからないですね。
なので、もう少し具体的な例を出してみます。

先ほど定義したAnchorコンポーネント、どこかで見覚えがないでしょうか?
Next.js の Link コンポーネントで同様のパターンを使っています。

https://nextjs-ja-translation-docs.vercel.app/docs/api-reference/next/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を子に渡すことで、ロジックを再利用可能な形に分割できます。

https://zenn.dev/morinokami/books/learning-patterns-1/viewer/render-props-pattern

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