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オブジェクト内のすべてのプロパティについて、返された要素はpropsからの値をelement.propsからの値よりも優先します。残りのプロパティは、元のelement.propsから入力されます。props.keyまたはprops.refを渡すと、それらは元のものを置き換えます。 -
オプション
...children: 0個以上の子ノード。Reactノードであれば何でも使用できます。React要素、文字列、数値、ポータル、空ノード(null、undefined、true、false)、およびReactノードの配列などが含まれます。...children引数を渡さない場合、元のelement.props.childrenが保持されます。
戻り値
cloneElement は、いくつかのプロパティを持つReact要素オブジェクトを返します。
type:element.typeと同じです。props:element.propsと、渡された上書き用のpropsを浅いマージした結果です。ref:props.refで上書きされていない限り、元のelement.refです。key:props.keyで上書きされていない限り、元のelement.keyです。
通常、コンポーネントから要素を返し、または別の要素の子要素にします。要素のプロパティを読み取ることはできますが、作成後の要素は不透明なものとして扱い、レンダリングのみを行うのが最善です。
注意事項
-
要素のクローン作成は元の要素を変更しません。
-
cloneElementに子要素を複数の引数として渡すのは、cloneElement(element, null, child1, child2, child3)のように、すべて静的に既知の場合のみです。子要素が動的な場合は、cloneElement(element, null, listItems)のように、配列全体を3番目の引数として渡します。これにより、Reactは動的なリストの欠落しているkeyに関する警告を表示します。静的なリストの場合、これらは並べ替えられることがないため、必要ありません。 -
cloneElementを使用するとデータフローの追跡が難しくなるため、代わりに代替手段を試してください。
使用方法
要素のプロパティの上書き
いくつかのReact要素のプロパティを上書きするには、上書きしたいプロパティを指定してcloneElementに渡します。
import { cloneElement } from 'react';
// ...
const clonedElement = cloneElement(
<Row title="Cabbage" />,
{ isHighlighted: true }
);ここで、結果として得られるクローンされた要素は<Row title="Cabbage" isHighlighted={true} />になります。
いつ役立つのかを理解するために、例を見てみましょう。
選択可能な行のリストと、選択されている行を変更する「次へ」ボタンで子要素をレンダリングするListコンポーネントがあるとします。Listコンポーネントは、選択されたRowを異なる方法でレンダリングする必要があるため、受信したすべての<Row>子要素をクローンし、追加のisHighlighted: trueまたはisHighlighted: falseプロパティを追加します。
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>「次へ」を押すとListの状態が更新され、異なる行が強調表示されることに注意してください。
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 />要素をクローンし、追加のプロパティを追加しました。
代替案
レンダープロップを使ったデータの受け渡し
cloneElement を使用する代わりに、renderItem のようなレンダープロップを受け入れることを検討してください。ここでは、List はプロップとして renderItem を受け取ります。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 プロップは、「何かをレンダリングする方法を指定するプロップ」であるため、「レンダープロップ」と呼ばれます。たとえば、指定された 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 は isHighlighted プロップを全く受け取る必要がありません。代わりに、コンテキストを読み取ります。
export default function Row({ title }) {
const isHighlighted = useContext(HighlightContext);
// ...これにより、呼び出し元コンポーネントは、isHighlighted を <Row> に渡す必要がなくなり、心配する必要もありません。
<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>
);
}データフローは明示的ですが、状態は任意のコンポーネントで使用できる 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> ); }
このアプローチは、異なるコンポーネント間でこのロジックを再利用したい場合に特に便利です。