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 元素。例如,它可以是 JSX 节点,如<Something />
,调用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="白菜" 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
的状态,并突出显示不同的行。
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 />
元素,并为它们添加了一个额外的属性。
替代方案
使用 render 属性传递数据
考虑接受一个像 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
更受欢迎,因为它更明确。
通过上下文传递数据
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> ); }
将逻辑提取到自定义 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> ); }
如果您想在不同的组件之间重用此逻辑,则此方法特别有用。