🐱

Reactのchildrenの型で子コンポーネントを制御する(したかった)

2022/06/28に公開2

はじめに

こんにちは猫です。

反響がけっこうあったので、用途を深掘ってみようと思います。が、先に訂正があります。
ReactElement<Props>で特定のコンポーネントを指定できると思っていたのですが、色々試したところコンポーネントの指定までは難しいようでした。早とちりですいません。
JSXに書かれたコンポーネントはJSX.Elementとなるため、詳細なPropsの型まではチェックしてくれないようです(ReactElement以外のFunctionComponentElementなど他の型も試したのですが、結果は変わらず)。

そのため、この記事では現状可能なことをまとめておきたいと思います。

Reactのchildren propsについて

まず@types/reactのv18からFunctionComponentなどに含まれていた暗黙的なchilrenが削除され、明示的に指定する必要が出てきます。
参考: https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#updates-to-typescript-definitions

そこで次の3パターンが主な指定の仕方になります。

import type { ReactNode } from 'react';

// ①childrenが必須なパターン
type Props = {
  children: ReactNode;
};

// ②childrenはあってもいいしなくてもいいパターン(これまで暗黙的に書かれていたものと同様)
type Props = {
  children?: ReactNode;
};

// ③childrenを持たないパターン
type Props = {
};

①のパターンを持つコンポーネントは次のように子コンポーネントがないと型エラーとなります。逆に③のパターンのコンポーネントでは子コンポーネントがあると型エラーになります。

①のパターン
const RequiredChildren: FunctionComponent<{ children: ReactNode }> = ({children}) => {
  return <>{children}</>
};

③のパターン
const NoChildren: FunctionComponent<{}> = () => {
  return <p>some content</p>;
};

const Main: FunctionComponent = () => {
  return (
    <>
      <RequiredChildren /> // エラーになる
      <NoChildren>content</NoChildren> // エラーになる
    </>
  );
};

基本的には①or③のパターンを使うことでこれまで書いてきたコンポーネントの型を置き換えることができると思われます。

childrenの型を指定して制御する

本題です。ReactNodeを指定した場合は広くなんでもchildrenとして扱うことができます。そこで型で子コンポーネントを制御するためにReactNode以外を使うパターンを考えてみます。

テキストのみを受け付けるコンポーネント

次のようにchildrenにstringを指定することで、子にはテキストのみを受け付けるコンポーネントを作れます。基本となるHTMLタグのみを定義したコンポーネントなどを作るケースが考えられます。

import type { FunctionComponent } from 'react';

type Props = {
  children: string;
};

const Strong: FunctionComponent<Props> = ({ children }) => {
  return <strong>{children}</strong>;
};
// OK
<Strong>text</Strong>

// 以下はエラー
<Strong><span>text</span></Strong>

<Strong><Link>text</Link></Strong>

子コンポーネントを1個だけ指定したいコンポーネント

ReactElement(またはJSX.Element)を使うことで、子コンポーネントを1つだけ持つことができます。また、テキストだけを持つことはできません。

import type { FunctionComponent, ReactElement } from 'react';

type Props = {
  children: ReactElement;
};

const Wrapper: FunctionComponent<Props> = ({ children }) => {
  return <>{children}</>;
};
// OK
<Wrapper>
  <Main>content</Main>
</Wrapper>

// 以下はエラー
<Wrapper></Wrapper>

<Wrapper>text</Wrapper>

<Wrapper>
  <Main>content</Main>
  <Footer>content</Footer>
</Wrapper>

childrenの個数が決まっているコンポーネント

NextUIのPopoverコンポーネントのように子コンポーネントに指定するものが決まっている場合に個数を指定することができます。

import type { ReactElement } from '@types/react';

type PopoverProps = {
  children: [ReactElement, ReactElement]
};
// OK
<Popover>
  <Popover.Trigger>
    <Button auto flat>Open Popover</Button>
  </Popover.Trigger>
  <Popover.Content>
    <Text>This is the content of the popover.</Text>
  </Popover.Content>
</Popover>

// 不足しているのでエラー
<Popover>
  <Popover.Trigger>
    <Button auto flat>Open Popover</Button>
  </Popover.Trigger>
</Popover>

// 多いのでエラー
<Popover>
  <Popover.Trigger>
    <Button auto flat>Open Popover</Button>
  </Popover.Trigger>
  <Popover.Content>
    <Text>This is the content of the popover.</Text>
  </Popover.Content>
  <div>Some Content</div>
</Popover>

まとめ

いくつかユースケースを考えてみましたが、知ってるとちょっと便利かなと思うものしか思いつかなかったです。本当は下記に書いたようなアイデアをやりたかったので、個人的には少し残念です。
@types/reactをv18に上げるときにchildrenの型を指定して回ると思うので、そのときにできる範囲で制限をかけていくのがよいかなと考えています。

もしコンポーネントまで制御する方法がわかった!という方がいらっしゃいましたらご教示いただけると嬉しいですmm


🪦供養 - こんなことができたらと妄想していたアイデア

親子関係が決まっているコンポーネント

ul, liのように子が決まっているコンポーネントの指定に利用したかった。
(サンプルのPropsだと広すぎて一致するコンポーネントが多そうですが、リアルワールドでは別のpropsも持っているはずなので、有効だろうと考えてました)

import type { FunctionComponent, ReactNode, ReactElement } from 'react';

type ListItemProps = {
  children: ReactNode;
};
const ListItem: FunctionComponent<ListItemProps> = ({ children }) => {
  return <li>{children}</li>;
};

type UnorderedListProps = {
  children: ReactElement<ListItemProps> | ReactElement<ListItemProps>[];
};
const UnorderedList: FunctionComponent<UnorderedListProps> = ({ children }) => {
  return <ul>{children}</ul>;
};
// OK
<UnorderedList>
  <ListItem>item 1</ListItem>
  <ListItem>item 2</ListItem>
</UnorderedList>

// エラーになってほしかった
<UnorderedList>
  <ListItem>item 1</ListItem>
  <ListItem>item 2</ListItem>
  <div>Some Content</div>
</UnorderedList>

HTML構造を担保したいコンポーネント

sectionタグにはheadingタグが必要というようなセクショニング・コンテンツを担保したい場合に利用したかった。

import { createElement } from 'react';
import type { FunctionComponent, ReactNode, ReactElement } from 'react';

type HeadingProps = {
  level: '1' | '2' | '3' | '4' | '5' | '6';
  children: string;
};
const Heading: FunctionComponent<HeadingProps> = ({ level, children }) => {
  return createElement(`h${level}`, {}, children);
};

type SectionProps = {
  children: [ReactElement<HeadingProps>, ...ReactNode[]];
};
const Section: FunctionComponent<SectionProps> = ({ children }) => {
  return <section>{children}</section>;
};
// OK
<Section>
  <Heading level="1">Title</Heading>
  <SomeContent />
  <SomeContent />
</Section>

// エラーになってほしかった
<Section>
  <SomeContent />
  <SomeContent />
</Section>

Discussion

HishoHisho

はじめまして!
型レベルでは難しそうですが、下記の記事の通りReactElement.type === ComponentNameComponentが特定できるのでコードベースでエラーは出せそうです😊

https://zenn.dev/takanori_is/articles/checking-child-element-specific-component-in-react

サンプル作ってみたのですがこのサンプルの実装がいいかと言われると微妙です🥲

mya-akemya-ake

はじめまして、コメント&サンプルまでありがとうございます!
ライブラリで利用しようと考えていたので、ReactElement.type === ComponentNameを使ってコンポーネント内のコードで制限して、適切な書き方はこうですというエラーメッセージを出すことができそうなのでよいなと思いました👍👍 ありがとうございます!