🏞️

React で <Stack.Item> みたいな作り方の使い所を考える

2021/05/04に公開

自分ではあまり使ったことがないのですが、最近他所のデザインシステムの実装なんかを眺めていて <Stack.Item> のようにあるコンポーネントのプロパティとして他のコンポーネントを定義するパターンを何度か見かけて、どんな時に有効なのかを考えてみたくなった次第です。

具体例

https://polaris.shopify.com/components/structure/stack#navigation

Shopify のデザインシステムの中にある StackStack.Item が当てはまります。
これらのコンポーネントは次のようにセットで使われます。

<Stack>
  <Stack.Item fill>
    <Heading>Order #1136</Heading>
  </Stack.Item>
  <Stack.Item>
    <Badge>Paid</Badge>
  </Stack.Item>
  <Stack.Item>
    <Badge>Fulfilled</Badge>
  </Stack.Item>
</Stack>

実装は次のようになっています。

// item.tsx
export function Item({children, fill}: ItemProps) {
  const className = classNames(styles.Item, fill && styles['Item-fill']);

  return <div className={className}>{children}</div>;
}

// stack.tsx
export const Stack = memo(function Stack({
  children,
  vertical,
  spacing,
  distribution,
  alignment,
  wrap,
}: StackProps) {
  const className = classNames(
    styles.Stack,
    vertical && styles.vertical,
    spacing && styles[variationName('spacing', spacing)],
    distribution && styles[variationName('distribution', distribution)],
    alignment && styles[variationName('alignment', alignment)],
    wrap === false && styles.noWrap,
  );

  const itemMarkup = elementChildren(children).map((child, index) => {
    const props = {key: index};
    return wrapWithComponent(child, Item, props);
  });

  return <div className={className}>{itemMarkup}</div>;
}) as NamedExoticComponent<StackProps> & {
  Item: typeof Item;
};

Stack.Item = Item;

最後の Stack.Item = Item; のところで Stack コンポーネントの Item プロパティとして Item コンポーネントを渡しています。

あるコンポーネントの children として渡すと機能するようなコンポーネントがある時に有効なパターンっぽいです。この例だと Stack.Item はおそらく親が Stack でないと意味がない、Stack に依存したコンポーネントなのでしょう。

メリット

セットで使うように意識づけできる

先ほど述べたように Item コンポーネントは Stack とセットで使われないと意味を成さないものなので、必ず Stack を import して使えないようにするのは変な使い方を禁止できていいのかなと思います。
※ 上記の例だと Item も export してるから単独で import できなくはないんですけど(private import があればなおベターそうですね)

Stack だけ import したら他の必要なコンポーネントがくっついてくる

これは瑣末な話ではありますが、 Stack だけ import したら Stack.Item もそのまま使えるなので一個 import したら他にも付いてくるのは一粒で二度美味しい感があります。

主語の大きい名前を付けられる

上記の例で言うと Item というのはなんとも主語の大きい名前で、コードレビューで見かけたら間違いなく「もっと詳細な名前をつけよう」と言いたくなるようなものですが、今回の場合は Stack.Item として参照されるのであまり気になりません。
結果ネームスペースを汚さずにシンプルな命名ができるのはメリットかなと思いました。

デメリット

何かあるかな?私は思いつかなかったので、もしあったらコメントください!

まとめ

あるコンポーネントに依存したコンポーネントを export して使いたい時に良さそうです。
今度それっぽい設計をしたくなったら使ってみようかなと思いました。

Discussion