複数のコンポーネントをワンセットで使いたい/使わせたい場面はしばしばあります。例えばTable関連のコンポーネントは、それぞれ別々に定義したほうが使い勝手は良いですが、tableタグの子要素や孫要素で発生したイベントなどはまとめてtableタグのpropsに渡すハンドラで処理したくなります。ここでは話を単純にするために、2階層の以下のような場面を想定します。
export default class App extends React.Component {
render() {
return (
<TableBody
onRowSelection={rows => {console.log(rows)}}
>
{this.props.users.map((user, i) => {
return (
<TableRow key={user.id}>
<TableRowColumn>{user.id}</TableRowColumn>
<TableRowColumn>{user.name}</TableRowColumn>
<TableRowColumn>{user.email}</TableRowColumn>
</TableRow>
);
})}
</TableBody>
);
}
}
TableBody
はtbodyタグを、TableRow
はtrタグを、TableRowColumn
はtdタグをそれぞれラップしています。TableRow
で発生したなんらかのイベントをTableBody
のpropsに渡したonRowSelectionハンドラで処理しているとします。
具体的には以下のようにします。
class TableBody extends React.Component {
static displayName = "TableBody";
public render() {
return (
<tbody>
{React.Children.map(this.props.children, (child, i) => {
if (
!React.isValidElement(child) ||
child.type.displayName !== TableRow.displayName
) {
console.warn(`Children of the TableBody component must be TableRow`);
return;
}
return React.cloneElement(
child as React.ReactElement,
{
rowNumber: i,
onRowSelection: (isChecked, rowNumber) => {
this.props.onRowSelection(isChecked, rowNumber);
},
}
);
})}
</tbody>
);
}
}
class TableRow extends React.Component {
static displayName = "TableRow";
render() {
return (
<tr>
<td>
<input type="checkbox" onChange={e => {
this.props.onRowSelection(e.target.checked, this.props.rowNumber);
}} />
</td>
{this.props.children}
</tr>
);
}
}
ReactのChildren.map
メソッドでchildrenをループで検査して、propsを渡したいコンポーネントならcloneElement
でpropsを追加します。Reactコンポーネントにstaticメンバを追加すると、親要素からchild.type.displayName
のようにアクセスできるので、それでコンポーネントを特定します。用意したコンポーネント以外を子要素に入れられたくない場合は、console.warn
などで警告を出してあげると親切です。