Menu

Reactのchildren:ラッパーコンポーネントとスロット

childrenは、コンポーネントの開始タグと終了タグの間に書いたものを保持するpropです。それを使ったラッパーやレイアウトコンポーネントの作り方、名前付きpropsで複数の枠を渡す方法、関数としてのchildren、そしてChildren APIが適さない場面を学びます。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

Reactの children は、コンポーネントの開始タグと終了タグの間に書いたものを保持するpropです。出力のどこかで {children} を描画するコンポーネントはラッパーになります。枠はコンポーネントが用意し、中身は呼び出す側が用意します。

2枚のカードは枠線、余白、見出しのスタイルを共有していますが、中身はまったく違います。リンクを入れた3枚目の <Card title="Help"> を追加すれば、同じ枠がそのまま手に入ります。

childrenがコンポーネントに届く仕組み

JSXはネストした中身をpropに変えます。次の2行は同じ要素を作ります。

<Card title="Profile"><p>Ada</p></Card>

<Card title="Profile" children={<p>Ada</p>} />

つまり children は、渡し方が特別なだけの普通のpropです。ほかのpropと同じように分割代入する(function Card({ children }))か、props.children として読みます。props全般についてはpropsのページで扱っています。

children に何が届くかは、タグの間に何を書いたかで決まります。子が1つならその子が、複数なら配列が届きます。テキストは文字列として届き、何もなければ undefined が届きます。

コンソールには object、array of 2、string、undefined と表示されます。普段これを気にする必要はほとんどありません。{children} を描画すればどの形にも対応でき、null、undefined、true、false は何も描画しないからです。形が問題になるのはchildrenを調べたり変えたりしようとするときだけで、最後のセクションはそれに注意を促しています。

レイアウトコンポーネント

children の最もよくある使い方はレイアウトコンポーネントです。ページの外枠、サイドバー付きのレイアウト、中央寄せのカラムなどです。コンポーネントが構造と余白を持ち、それを使う各ページが自分の中身を渡します。

function PageLayout({ children }) {
    return (
        <div className="page">
            <Header />
            <main className="page-content">{children}</main>
            <Footer />
        </div>
    );
}

function AboutPage() {
    return (
        <PageLayout>
            <h1>About us</h1>
            <p>We teach people to code.</p>
        </PageLayout>
    );
}

PageLayout は、紹介ページに何が入っているかを知りませんし、気にもしません。この分離こそが目的です。ヘッダーを1回変えれば、このレイアウトで包まれたすべてのページに反映されます。

名前付きpropsで複数の枠を作る

children は1つの枠です。コンポーネントが複数の場所に中身を必要とするときは、追加の部品を名前付きのpropsとして渡します。どのpropもJSXを保持できるので、モーダルは title、footer、そして本文を children として受け取れます。

モーダルの外枠が3つの領域の枠線、余白、順番を管理し、呼び出す側がそれぞれを埋めます。ほかのフレームワークではこれをスロットと呼びますが、Reactではただのpropsです。2つのボタンを囲む <>...</> はフラグメントで、余分な要素を追加せずにまとめます。本物のダイアログなら、ページの上に重なるようにポータルで document.body に描画するでしょう。

<p> を title propに移すと、本文のテキストがヘッダーに表示されます。各propがどこに置かれるかはコンポーネントが決め、呼び出す側はそこに何を入れるかだけを決めるのです。

関数としてのchildren

ラッパーがstateを持ち、それをどう描くかは呼び出す側に決めさせたいことがあります。その場合、呼び出す側は要素の代わりに関数を children として渡し、ラッパーがその値を引数にして関数を呼びます。このパターンはレンダープロップと呼ばれます。

同じロジックから、一方の Toggle はボタンを、もう一方はチェックボックスを描画しています。フックが登場する前は、レンダープロップがstateを持つロジックを共有する主な方法でした。今では useToggle() のようなカスタムフックのほうがたいてい読みやすいですが、リスト、フォーム、アニメーションのライブラリでは今もレンダープロップを見かけます。

Children APIとcloneElement

Reactは children をたどるための補助関数を持つ Children オブジェクトをエクスポートしています。Children.map、Children.forEach、Children.count、Children.toArray、Children.only です。要素を新しいpropsでコピーする cloneElement と組み合わせると、親は渡されたものを調べたり変えたりできます。ReactチームはこれらをレガシーAPIとして挙げています。今も動きますが、新しいコードでは避けるべきです。

理由は、これらがタグの間に直接書かれた要素しか見えないことです。その要素が描画するものは見えません。

リストには4つの項目が表示されますが、コンソールには Children.count = 3 と出ます。TwoMore は、いくつ項目を描画しても1つの子だからです。cloneElement は style propを TwoMore にも渡しますが、TwoMore はそれを無視するので、3つ目と4つ目の項目は縞模様になりません。誰かがいくつかの <li> 要素をコンポーネントにまとめただけで、親は触られていないのに壊れてしまいます。

解決策は、childrenの中に手を入れるのをやめて、代わりにデータを渡すことです。

function NumberedList({ items }) {
    return (
        <ol>
            {items.map((item, i) => (
                <li key={item.id} style={{ color: i % 2 ? 'gray' : 'black' }}>
                    {item.label}
                </li>
            ))}
        </ol>
    );
}

これで各行の描画はリストが担当し、呼び出す側がJSXをどう書いたかに依存するものはなくなります。親が深くネストした子と値を共有する必要があるとき(選択中のタブ、テーマなど)は、propsをクローンして付けるのではなくコンテキストを使ってください。

TypeScriptでchildrenに型を付ける

TypeScriptでは、children の型を React.ReactNode にします。要素、文字列、数値、それらの配列、null、undefined、真偽値など、Reactが描画できるものすべてを含みます。

import type { ReactNode } from 'react';

type CardProps = {
    title: string;
    children: ReactNode;
};

function Card({ title, children }: CardProps) {
    return (
        <section>
            <h3>{title}</h3>
            {children}
        </section>
    );
}

レンダープロップの場合は、実際の関数の型を付けます:children: (on: boolean, toggle: () => void) => ReactNode。中身が空でも成り立つコンポーネントなら、children を省略可能(children?: ReactNode)にしてください。

よくある間違い

childrenを描画し忘れる。childrenを受け取るのに出力に {children} を入れないと、中身は黙って消えます。警告は何も出ません。

関数でないchildrenを関数として呼ぶ。children() が動くのは、呼び出す側が関数を渡したときだけです。コンポーネントがレンダープロップを期待するなら、名前やドキュメントでそれを示すか、意図が明確になるよう render のような名前付きpropを受け取ってください。

childrenを変更する。要素は読み取り専用です。開発環境ではReactが要素を凍結するので、children.props に代入すると例外が発生します。代わりに新しい出力を作ってください。

よくある質問

Reactのprops.childrenとは何ですか?

コンポーネントの開始タグと終了タグの間に書かれた中身です。<Card><p>Hi</p></Card> では、Card 関数は <p> を props.children として受け取り、どこに描画するかを決めます。

childrenは特別なpropですか?

特別なのは渡し方だけです。JSXはネストした中身を children というpropに入れますが、コンポーネントの中では普通のpropです。<Card children={<p>Hi</p>} /> のように明示的に渡しても同じように動きます。

コンポーネントに複数の中身のブロックを渡すには?

追加のブロックには名前付きのpropsを使います。propは children と同じようにJSXを保持できるので、<Modal title={<h2>Delete?</h2>} footer={<button>OK</button>}>Body</Modal> でコンポーネントに3つの枠を与えられます。

Children.mapやcloneElementは使うべきですか?

新しいコードでは避けてください。これらはタグの間に直接書かれた要素しか見えず、その要素が描画するものは見えません。そのため、誰かが子を別のコンポーネントで包んだ途端に壊れます。代わりにデータの配列をpropとして渡すか、コンテキストを使ってください。

TypeScriptでchildrenの型は何ですか?

React.ReactNode を使います。要素、文字列、数値、配列、null、undefined、真偽値など、Reactが描画できるものすべてを含みます。

Coddyのプログラミング言語のイラスト

Coddyでコードを学ぼう

始める