cloneElement
cloneElement
を使用すると、別の要素に基づいて新しい React 要素を作成することができます。
const clonedElement = cloneElement(element, props, ...children)
リファレンス
cloneElement(element, props, ...children)
cloneElement
を呼び出して、element
を基に、異なる props
と children
を持った React 要素を作成します。
import { cloneElement } from 'react';
// ...
const clonedElement = cloneElement(
<Row title="Cabbage">
Hello
</Row>,
{ isHighlighted: true },
'Goodbye'
);
console.log(clonedElement); // <Row title="Cabbage" isHighlighted={true}>Goodbye</Row>
引数
-
element
:element
引数は有効な React 要素でなければなりません。例えば、<Something />
のような JSX ノード、createElement
の呼び出し結果、または別のcloneElement
の呼び出し結果などです。 -
props
:props
引数はオブジェクトかnull
でなければなりません。null
を渡すと、クローンされた要素は元のelement.props
をすべて保持します。それ以外の場合、props
オブジェクト内のすべての項目について、返される要素ではelement.props
の値よりもprops
からの値が「優先」されます。残りの props は元のelement.props
から埋められます。props.key
やprops.ref
を渡した場合、それらは元のものを置き換えます。 -
省略可能
...children
: ゼロ個以上の子ノード。あらゆる React ノード、つまり React 要素、文字列、数値、ポータル、空ノード(null
、undefined
、true
、false
)、React ノードの配列になります。...children
引数を渡さない場合、元のelement.props.children
が保持されます。
返り値
cloneElement
は以下のプロパティを持つ React 要素オブジェクトを返します。
type
:element.type
と同じ。props
:element.props
に、渡された上書き用のprops
を浅くマージした結果。ref
: 元のelement.ref
。ただし、props.ref
によって上書きされた場合は除く。key
: 元のelement.key
。ただし、props.key
によって上書きされた場合は除く。
通常、この要素をコンポーネントから返すか、他の要素の子として用います。要素のプロパティを読み取ることは可能ですが、作成後は要素の構造を非公開 (opaque) として扱い、レンダーのみ行うようにするべきです。
注意点
-
要素をクローンしても元の要素は変更されません。
-
複数の子の内容がすべて静的に分かっている場合、
cloneElement
には子をcloneElement(element, null, child1, child2, child3)
のように複数の引数として渡してください。子が動的な場合は、配列全体を第 3 引数としてcloneElement(element, null, listItems)
のように渡してください。これにより、React は動的なリストにkey
が欠けている場合に警告を出すようになります。静的なリストでは並び替えは決して発生しないため、key は必要ありません。 -
cloneElement
を使うとデータフローの追跡が難しくなるため、代わりに代替手段を試してみてください。
使用法
要素の props を上書きする
React 要素 の props を上書きするには、それを cloneElement
に渡し、上書きしたい props を指定します。
import { cloneElement } from 'react';
// ...
const clonedElement = cloneElement(
<Row title="Cabbage" />,
{ isHighlighted: true }
);
この場合、結果となるクローンされた要素は <Row title="Cabbage" isHighlighted={true} />
になります。
例を使って、これが役立つ場面を見てみましょう。
選択可能な行のリストと、選択されている行を変更する “Next” ボタンをレンダーする List
コンポーネントを想像してみてください。List
コンポーネントは、選択された Row
を異なる方法でレンダーする必要があるため、受け取ったすべての <Row>
をクローンし、isHighlighted: true
または isHighlighted: false
を追加の props として指定します。
export default function List({ children }) {
const [selectedIndex, setSelectedIndex] = useState(0);
return (
<div className="List">
{Children.map(children, (child, index) =>
cloneElement(child, {
isHighlighted: index === selectedIndex
})
)}
例えば List
が受け取る元の JSX が以下のようなものである場合を考えます。
<List>
<Row title="Cabbage" />
<Row title="Garlic" />
<Row title="Apple" />
</List>
子要素をクローンすることで、List
は内部のすべての Row
に追加情報を渡すことができます。結果は以下のようになります。
<List>
<Row
title="Cabbage"
isHighlighted={true}
/>
<Row
title="Garlic"
isHighlighted={false}
/>
<Row
title="Apple"
isHighlighted={false}
/>
</List>
“Next” を押すと List
の state が更新され、異なる行がハイライトされることに着目してください。
import { Children, cloneElement, useState } from 'react'; export default function List({ children }) { const [selectedIndex, setSelectedIndex] = useState(0); return ( <div className="List"> {Children.map(children, (child, index) => cloneElement(child, { isHighlighted: index === selectedIndex }) )} <hr /> <button onClick={() => { setSelectedIndex(i => (i + 1) % Children.count(children) ); }}> Next </button> </div> ); }
おさらいすると、List
は受け取った <Row />
要素をクローンし、それらに追加の props を付加したということです。
代替手段
レンダープロップを用いてデータを渡す
cloneElement
を使用する代わりに、renderItem
のようなレンダープロップ (render prop) を受け取るようにすることを検討してみてください。以下の例では、List
は renderItem
を props として受け取ります。List
は各アイテムに対して renderItem
を呼び出し、isHighlighted
を引数として渡します。
export default function List({ items, renderItem }) {
const [selectedIndex, setSelectedIndex] = useState(0);
return (
<div className="List">
{items.map((item, index) => {
const isHighlighted = index === selectedIndex;
return renderItem(item, isHighlighted);
})}
renderItem
のようなものは「レンダープロップ」と呼ばれます。何かをレンダーする方法を指定するための props だからです。例えば、与えられた isHighlighted
の値で <Row>
をレンダーする renderItem
の実装を渡すことができます。
<List
items={products}
renderItem={(product, isHighlighted) =>
<Row
key={product.id}
title={product.title}
isHighlighted={isHighlighted}
/>
}
/>
最終的な結果は cloneElement
と同じです。
<List>
<Row
title="Cabbage"
isHighlighted={true}
/>
<Row
title="Garlic"
isHighlighted={false}
/>
<Row
title="Apple"
isHighlighted={false}
/>
</List>
しかし、isHighlighted
値がどこから来ているかを明確に追跡することができます。
import { useState } from 'react'; export default function List({ items, renderItem }) { const [selectedIndex, setSelectedIndex] = useState(0); return ( <div className="List"> {items.map((item, index) => { const isHighlighted = index === selectedIndex; return renderItem(item, isHighlighted); })} <hr /> <button onClick={() => { setSelectedIndex(i => (i + 1) % items.length ); }}> Next </button> </div> ); }
このパターンはより明示的であるため、cloneElement
よりも推奨されます。
コンテクストでデータを渡す
cloneElement
の別の代替手段としてコンテクストを通じてデータを渡すことが可能です。
例として、createContext
を呼び出して HighlightContext
を定義しましょう。
export const HighlightContext = createContext(false);
List
コンポーネントは、レンダーするすべてのアイテムを HighlightContext
プロバイダでラップします。
export default function List({ items, renderItem }) {
const [selectedIndex, setSelectedIndex] = useState(0);
return (
<div className="List">
{items.map((item, index) => {
const isHighlighted = index === selectedIndex;
return (
<HighlightContext.Provider key={item.id} value={isHighlighted}>
{renderItem(item)}
</HighlightContext.Provider>
);
})}
このアプローチでは、Row
は props で isHighlighted
を受け取る必要が一切ありません。代わりにコンテクストから読み取ります。
export default function Row({ title }) {
const isHighlighted = useContext(HighlightContext);
// ...
これにより、呼び出し元のコンポーネントは <Row>
に isHighlighted
を渡すことについて知る必要も、気にする必要もなくなります。
<List
items={products}
renderItem={product =>
<Row title={product.title} />
}
/>
代わりに、List
と Row
はコンテクストを通じ、ハイライトのロジックに関して協調して動作します。
import { useState } from 'react'; import { HighlightContext } from './HighlightContext.js'; export default function List({ items, renderItem }) { const [selectedIndex, setSelectedIndex] = useState(0); return ( <div className="List"> {items.map((item, index) => { const isHighlighted = index === selectedIndex; return ( <HighlightContext.Provider key={item.id} value={isHighlighted} > {renderItem(item)} </HighlightContext.Provider> ); })} <hr /> <button onClick={() => { setSelectedIndex(i => (i + 1) % items.length ); }}> Next </button> </div> ); }
ロジックをカスタムフックに抽出する
試すべき別のアプローチは、「非視覚的」なロジックを自前のフックに抽出し、フックから返される情報を使用して何をレンダーするかを決定することです。例えば次のような useList
カスタムフックを書くことができます。
import { useState } from 'react';
export default function useList(items) {
const [selectedIndex, setSelectedIndex] = useState(0);
function onNext() {
setSelectedIndex(i =>
(i + 1) % items.length
);
}
const selected = items[selectedIndex];
return [selected, onNext];
}
これを以下のように使用できます。
export default function App() {
const [selected, onNext] = useList(products);
return (
<div className="List">
{products.map(product =>
<Row
key={product.id}
title={product.title}
isHighlighted={selected === product}
/>
)}
<hr />
<button onClick={onNext}>
Next
</button>
</div>
);
}
データフローは明示的ですが、state は任意のコンポーネントから使用できる useList
カスタムフック内にあります。
import Row from './Row.js'; import useList from './useList.js'; import { products } from './data.js'; export default function App() { const [selected, onNext] = useList(products); return ( <div className="List"> {products.map(product => <Row key={product.id} title={product.title} isHighlighted={selected === product} /> )} <hr /> <button onClick={onNext}> Next </button> </div> ); }
このアプローチは、特にこのロジックを異なるコンポーネント間で再利用したい場合に有用です。