Reactでrender propを使って子コンポーネントに任意の要素を渡す方法
はじめに
皆さんはReactで子コンポーネントに任意の要素を渡す場合はどうしていますか?おそらくchildrenを使っていると思います。私もモーダルを実装する場合などによく使います。
では、以下の場合はどうでしょうか?SampleListコンポーネントはコメントで書いた「任意の要素」の部分に<p>
や<input>
、場合によっては<select>
を表示することもある一覧表示用のコンポーネントになります。<select>
を表示する際には<option>
に表示する選択肢も必要になり、「任意の要素」で表示する内容によってはSampleListのpropsも余分に増えてしまいそうですよね。このような場合に使えるrender propについて今回はご紹介します。
interface Item {
key: string;
value: string;
}
export const SampleList = ({
itemList,
}: {
itemList: Item[];
}) => {
return (
<div>
{itemList.map((item, index) => {
return (
// 任意の要素
);
})}
</div>
);
};
render propの使い方
早速ですが冒頭のSampleListにrender propを適用してみました。簡単に説明すると「propsにJSXを返す関数(render)」を新しく追加した状態です。これがrender propになります。renderの引数についてはその時々で必要なものを指定すればOKです。
interface Item {
key: string;
value: string;
}
export const SampleList = ({
itemList,
render,
}: {
itemList: Item[];
render: (item: Item, index: number) => ReactNode;
}) => {
return (
<div>
{itemList.map((item, index) => {
return (
render(item, index)
);
})}
</div>
);
};
呼び出し方もその時々によりますが、<input>
を使う場合は以下のようなります。handleChange関数は各inputの内容を更新する関数になります。
render propを使ったことで
・更新処理:呼び出し元で管理
・一覧表示:SampleListコンポーネントで制御
と関心を分離できていて良いですね。
interface ListItem {
key: string;
value: string;
}
export const SamplePage = () => {
const [list, setList] = useState<ListItem[]>([
{ key: '1', value: '' },
{ key: '2', value: '' },
{ key: '3', value: '' },
]);
const handleChange = (index: number, value: string) => {
const newList = [ ...list ];
newList[index].value = value;
setList([ ...newList ]);
};
return (
<SampleList
itemList={list}
render={(item, index) => {
return <input value={item.value} onChange={(e) => handleChange(index, e.target.value)}/>;
}}
/>
);
};
また、render={}の部分は関数を渡しているだけなので、関数の部分を以下のように切り出すこともできます。見た目がスッキリしましたね。
const render = (item: Item, index: number) => {
return (
<input value={item.value} onChange={(e) => handleChange(index, e.target.value)} />
)
}
return (
<SampleList
itemList={list}
render={render}
/>
);
みなみに、冒頭で話題に出した<select>
を指定する場合は以下のようになります。こちらも同様に<option>
に表示する選択肢もhandleChange関数も呼び出し元で管理しています。SampleListコンポーネントは一覧表示に専念した状態を保つことができていて良いですね。
interface ListItem {
key: string;
value: string;
}
const selectItemList = [
{ key: '1', value: 'value1' },
{ key: '2', value: 'value2' },
{ key: '3', value: 'value3' },
]
export const SamplePage = () => {
const [list, setList] = useState<ListItem[]>([
{ key: '1', value: '' },
{ key: '2', value: '' },
{ key: '3', value: '' },
]);
const handleChange = (index: number, value: string) => {
const newList = [ ...list ];
newList[index].value = value;
setList([ ...newList ]);
}
const render = (item: Item, index: number) => {
return (
<select>
<option key={'0'} value={''}>選択してください</option>
{selectItemList.map((select) => {
return (
<option
key={select.key}
value={select.value}
selected={item.value === select.value}
onChange={(e) => handleChange(index, e.target.value)}
>
{select.value}
</option>
);
})}
</select>
)
}
return (
<SampleList
itemList={list}
render={render}
/>
);
};
余談
余談ですが冒頭ではあえて「このような場合にはchildrenは使えません」とは書かないようにしています。実は以下のように指定することでchildrenでもrender propを使えます。事情を知らないプロジェクトのメンバーが見ると「なんでこのchildrenは関数型なんだろう?」と悩ませてしまうので、私は使わないようにしていますが小ネタとして紹介させていただきます。
// render → childrenに書き換えたSampleListコンポーネント
export const SampleList = ({
itemList,
children,
}: {
itemList: Item[];
children: (item: Item, index: number) => ReactNode;
}) => {
return (
<div>
{itemList.map((item, index) => {
return (
children(item, index)
);
})}
</div>
);
};
// 呼び出し元
export const SamplePage = () => {
const render = (item: Item, index: number) => {
return (
// 省略
)
}
return (
<SampleList
itemList={list}
>
{render}
</SampleList>
);
};
Discussion