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が描画できるものすべてを含みます。