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> ); }
このアプローチは、異なるコンポーネント間でこのロジックを再利用したい場合に特に便利です。