ReactのJSXでも子要素を定義できますが、これが結構面白いものでした。
自作エレメントにも子要素
ReactのJSXでは、(DOMを組み立てるものである以上当然ですが)<div>の中に<a>を書いて、そしてさらに文字列を書き込む、ということが可能です。
そして、これはHTML由来のエレメントだけでなく、自作のコンポーネントでも実現可能です。
function SomeWrapper({children}) {
return(
<div className="some-class">
{ children }
</div>
);
}
このように、子要素はchildrenというPropとして渡ってきます。
childrenの中身が知りたくて
では、このchildrenには何がどのような形式で来るのでしょうか。JSXの変換先であるReact.createElementのソースコードに当たってみました。挙動はchildrenの数によって違います。
- 0個…
childrenにはpropとして渡したchildrenが(もしあれば)渡される - 1個…
childrenには唯一のchildren引数がそのまま渡される1 - 2個以上…
childrenには引数で渡されたものが配列に詰め込まれる
公式な操作方法
現実問題として、今からchildrenの実装を変えてしまうということは互換性問題などを考えれば可能性は薄いのですが、公式にはchildrenのデータ形式は「非公開」ということになっています。そこで、childrenを操作するためのReact.Childrenというユーティリティ関数群があります(リファレンス)。
-
React.Children.map…childrenの各要素に対して関数を実行して、結果を配列で得る。 -
React.Children.forEach…childrenの各要素に対して関数を実行する。 -
React.Children.count…childrenの個数を返す -
React.Children.toArray…childrenを本物の配列に変換して返す -
React.Children.only…childrenが1つのJSXエレメントであることを保証する
なお、forEach、map、toArrayで処理する際に、キーは子要素間で一意となるようなものがReact側で振られます。また、子要素として<React.Fragment>を渡した場合、1つものとして扱われます。
childrenとReact.memo
今のところ、React.createElementをキャッシュするような仕組みはないので、<br />のようなシンプルなJSX要素であっても、2回の実行でオブジェクトとして一致することはありません。さらに、上述のように複数の子要素をもたせた場合、childrenの配列を生成しますので、このインスタンスも一致しません。
そして、PureComponentやReact.memoはchildrenを特別扱いしませんので、(文字列1つだけ指定するような例外的なケースは別として)childrenを指定するようなコンポーネントでは、メモ化は利かないということになってしまいます。
対策としては、
- 子要素を取るコンポーネントを使う側の場合、「コンポーネント+子要素」全体を1つのコンポーネントとして、それをメモ化する
- 子要素を取るコンポーネントを作る場合、メモ化を諦める
などが考えられます。そして、children以外でも、表示する文字列をpropで取る場合に、「HTMLタグで装飾したい」とその場で書いたJSXを渡せば同じ事態となります。こちらについては、React.useMemo、クラスのプロパティ、外側で変数に定義するなど、同じJSXインスタンスを使い回す形として対応が可能かと思います。
-
Context.Consumerなどで子要素代わりにコールバックを書くことがありますが、1個なので関数がそのままchildrenに入る形となっています。 ↩