cloneElement
cloneElement
允許您使用另一個元素作為起始點來建立新的 React 元素。
const clonedElement = cloneElement(element, props, ...children)
參考
cloneElement(element, props, ...children)
呼叫 cloneElement
根據 element
建立一個 React 元素,但具有不同的 props
和 children
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
:零個或多個子節點。它們可以是任何 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
覆寫。
通常,您會從您的組件回傳元素,或使其成為另一個元素的子元素。雖然您可以讀取元素的屬性,但在建立元素後,最好將每個元素視為不透明的,並且只渲染它。
注意事項
-
複製元素不會修改原始元素。
-
您應該僅將子元素作為多個參數傳遞給
cloneElement
,如果它們都是靜態已知的,例如cloneElement(element, null, child1, child2, child3)
。如果您的子元素是動態的,請將整個陣列作為第三個參數傳遞:cloneElement(element, null, listItems)
。這可以確保 React 會針對任何動態列表警告您缺少key
。對於靜態列表,這不是必需的,因為它們永遠不會重新排序。 -
cloneElement
會使追蹤資料流變得更加困難,因此請嘗試使用替代方案。
用法
覆寫元素的屬性
要覆寫某些 React 元素 的屬性,請將其與 您想要覆寫的屬性一起傳遞給 cloneElement
import { cloneElement } from 'react';
// ...
const clonedElement = cloneElement(
<Row title="Cabbage" />,
{ isHighlighted: true }
);
這裡,產生的 複製元素 將會是 <Row title="Cabbage" isHighlighted={true} />
。
讓我們來看一個例子,了解它在什麼時候有用。
想像一個 List
組件,它將其 children
渲染為可選取列的列表,並帶有一個「下一個」按鈕,可以更改哪一行被選取。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
的狀態,並 highlight 不同的列
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 />
元素,並為它們添加了一個額外的屬性。
替代方案
使用渲染屬性傳遞資料
考慮使用像 renderItem
這樣的*渲染屬性*,而不是使用 cloneElement
。在這裡,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
屬性被稱為「渲染屬性」,因為它是一個指定如何渲染某些內容的屬性。例如,您可以傳遞一個 renderItem
實作,它使用給定的 isHighlighted
值來渲染 <Row>
。
<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
更受歡迎,因為它更明確。
透過 Context 傳遞資料
cloneElement
的另一個替代方案是透過 Context 傳遞資料。
例如,您可以呼叫 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
屬性。相反,它會讀取 Context。
export default function Row({ title }) {
const isHighlighted = useContext(HighlightContext);
// ...
這允許呼叫元件不必知道或擔心將 isHighlighted
傳遞給 <Row>
。
<List
items={products}
renderItem={product =>
<Row title={product.title} />
}
/>
相反,List
和 Row
透過 Context 協調突顯邏輯。
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> ); }
將邏輯提取到自訂 Hook
您可以嘗試的另一種方法是將「非視覺」邏輯提取到您自己的 Hook 中,並使用 Hook 返回的資訊來決定要渲染的內容。例如,您可以編寫一個像這樣的 useList
自訂 Hook。
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
自訂 Hook 內。
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> ); }
如果您想在不同的元件之間重複使用此邏輯,這種方法特別有用。