Reactコンポーネントでchildrenに対してpropsを渡す
Reactで子コンポーネントにpropsを渡す2つの方法を紹介します。レガシーなcloneElement APIと、推奨されるrender propパターンの違いを解説し、ツールチップの実装例を通じて実践的な使い方を学びます。
はじめに
childrenとして任意のコンポーネントを受け取る親コンポーネントから、子コンポーネントにpropsを渡したいことがあります。
import { FC, PropsWithChildren } from 'react';
const ParentComponent: FC<PropsWithChildren> = ({
children,
}) => (
<div>
{/* childrenにonClick等のpropsを親から付与したい */}
{children}
</div>
);
この記事では、これを実現する2つの方法と、それを活用したツールチップの実装例を紹介します。
cloneElement
ここで紹介するcloneElementとisValidElementはReactのレガシーAPIです。推奨されるAPIではないため新しく実装する場合はこの後に紹介するrender propパターンを使ってください。
最初に紹介する方法はcloneElementです。cloneElementは対象のReactElementを複製する際に、第2引数でpropsを上書きできます。
子コンポーネントにonClickを付与する例は以下のようになります。
import { cloneElement, FC, isValidElement, PropsWithChildren } from 'react';
const ParentComponent: FC<PropsWithChildren> = ({ children }) => {
const cloneChildren = cloneElement(
isValidElement(children) ? children : <button>{children}</button>,
{
onClick: () => alert('親から付与されたclickイベントです。'),
}
);
return <div>{cloneChildren}</div>;
};
cloneElementの第1引数はReactElementを渡したいので、ReactNodeからisValidElementを用いてReactElementを抽出しました。
childrenがReactElementではなかった場合はbutton要素で囲んだものを渡すようにしています。
第2引数には上書きしたいpropsをオブジェクトで渡します。ただし、この方法ではchildrenが元々持っていたpropsが全て置き換えられる点に注意してください。
元のpropsを保持したい場合は以下のようにします。
const cloneChildren = isValidElement(children)
? cloneElement(children, {
// この順番だと親のonClickが優先される
// 子要素のpropsを優先したい場合は順番を逆にする
...children.props,
onClick: () => alert('親から付与されたclickイベントです。'),
})
: cloneElement(<button>{children}</button>, {
// ReactElementじゃないときは`button`に対して付与されるので考慮不要
onClick: () => alert('親から付与されたclickイベントです。'),
});
Slotで隠蔽する
cloneElementはレガシーAPIですが、Radix Primitivesが提供するSlotコンポーネントを使うことで、アプリケーションコードから直接呼び出すことを避けられます。
Slotは内部でcloneElementを使用していますが、より宣言的なAPIを提供します。
import { Slot } from '@radix-ui/react-slot';
import { FC, PropsWithChildren } from 'react';
const ParentComponent: FC<PropsWithChildren> = ({ children }) => {
return (
<Slot onClick={() => alert('親から付与されたclickイベントです。')}>
{children}
</Slot>
);
};
Slotは子要素に自身のpropsをマージして渡します。これにより、cloneElementを直接使用するよりも可読性が高く、型安全なコードを書けます。
render propパターン
続いてはrender propパターン呼ばれるpropsを利用した実装です。
親コンポーネントはrenderItemという関数をpropsとして受け取り、渡したいpropsを引数にしてその関数を呼び出します。
import { FC, ReactElement } from 'react';
type Props = {
renderItem: (props: { onClick: () => void }) => ReactElement;
};
const ParentComponent: FC<Props> = ({ renderItem }) => {
return (
<div>
{renderItem({
onClick: () => alert('親から付与されたclickイベントです。'),
})}
</div>
);
};
renderItemはpropsを引数に取り、ReactElementを返す関数です。利用側は受け取ったpropsを要素に適用して返します。
<ParentComponent
renderItem={(props) => (
<button {...props}>
親コンポーネントから渡ってきたonClickを付与したbutton要素
</button>
)}
/>
この方法では指定したいpropsを子コンポーネント側に明示的に渡せることや子コンポーネントの実装を自身でハンドリングできるところに利点があります。
おわりに
この記事では、childrenにpropsを渡す方法としてcloneElementとrender propパターンを紹介しました。
cloneElementはレガシーAPIのため、直接使用するのではなく@radix-ui/react-slotのSlotで隠蔽することをおすすめします。また、型安全で明示的なrender propパターンも検討してみてください。