☑️

childrenに任意なコンポーネントしか渡せないよう縛りを

2024/07/28に公開

今回やること

例えばTabListというコンポーネントのchildrenにはTabしか渡せないようにしたい⚠️

<TabList>
  <Tab val="1" />
  <Tab val="2" />
  <Tab val="3" />
</TabList>

下記のようにすると、TabList children must be Tabというエラーをスルーさせよう。

<TabList>
  <Tab val="1" />
  <div />
</TabList>

childrenがTabじゃない場合はエラー

※無関係なコードは省いている最低限のものです

import { Tab } from './Tab';

type TabProps = ComponentProps<typeof Tab>;
type TabReactElement = ReactElement<TabProps>;

interface Props {
  children: TabReactElement | Array<TabReactElement>;
}

export const TabList: FC<Props> = ({ children }) => {

  // 下記がchildrenがTabでない場合はエラーを出す記述
  // 開発環境のみチェックや、記述場所工夫してもよさそう
  Children.forEach(children, (child) => {
    if (child.type !== Tab) {
      throw new Error('TabList children must be Tab');
    }
  });

  return (
    <ul role="tablist">
      {children}
    </ul>
  );

開発時に気付ければいいので、isDevなどだったらチェックするようにしても良さそうです。

React.Childrenを覗く

Childrenは下記のようになっており、mapなどのプロパティも存在するので、childrenのpropsを加工したりもできます。

// Sync with `ReactChildren` until `ReactChildren` is removed.
const Children: {
    map<T, C>(children: C | ReadonlyArray<C>, fn: (child: C, index: number) => T):
        C extends null | undefined ? C : Array<Exclude<T, boolean | null | undefined>>;
    forEach<C>(children: C | ReadonlyArray<C>, fn: (child: C, index: number) => void): void;
    count(children: any): number;
    only<C>(children: C): C extends any[] ? never : C;
    toArray(children: ReactNode | ReactNode[]): Array<Exclude<ReactNode, boolean | null | undefined>>;
};

下記は、TabonClickを加工する例です。

const tabs = Children.map(children, (child) => {
  return cloneElement(child, {
    onClick: handleTabClick.bind(null, child.value),
    isSelected: child.props.value === selectedTab,
  });
});

デザインシステムなどで、自前のコンポーネントだけchildrenに渡せるようにしたい!だったり、コーディング規約だと守りきれないと思うので、コードでチェックできると便利ですね✨

Discussion