cloneElement
cloneElement 让你可以使用另一个元素作为起点来创建一个新的 React 元素。
const clonedElement = cloneElement(element, props, ...children)参考
🌐 Reference
cloneElement(element, props, ...children)
调用 cloneElement 根据 element 创建一个 React 元素,但使用不同的 props 和 children:
🌐 Call cloneElement to create a React element based on the element, but with different props and 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>参数
🌐 Parameters
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将被保留。
返回
🌐 Returns
cloneElement 返回一个带有几个属性的 React 元素对象:
type:与element.type相同。props:将element.props与你传入的覆盖props浅合并的结果。ref:原始的element.ref,除非它被props.ref覆盖。key:原始的element.key,除非它被props.key覆盖。
通常,你会从你的组件中返回元素,或者将其作为另一个元素的子元素。虽然你可以读取元素的属性,但最好在元素创建后将其视为不透明,只进行渲染。
🌐 Usually, you’ll return the element from your component or make it a child of another element. Although you may read the element’s properties, it’s best to treat every element as opaque after it’s created, and only render it.
注意事项
🌐 Caveats
- 克隆一个元素不会修改原始元素。
- 你应该仅**在所有子元素都是静态已知的情况下,将它们作为多个参数传递给
cloneElement,**就像cloneElement(element, null, child1, child2, child3)一样。如果你的子元素是动态的,请将整个数组作为第三个参数传递:cloneElement(element, null, listItems)。这样可以确保 React 会警告你任何动态列表中缺失的key。对于静态列表则不需要这样做,因为它们从不重新排序。 cloneElement使跟踪数据流更加困难,所以尝试使用替代方案吧。
用法
🌐 Usage
覆盖元素
🌐 Overriding props of an element
要覆盖某个 React元素的props,将其与你想要覆盖的 props一起传递给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 属性:
🌐 Imagine a List component that renders its children as a list of selectable rows with a “Next” button that changes which row is selected. The List component needs to render the selected Row differently, so it clones every <Row> child that it has received, and adds an extra isHighlighted: true or isHighlighted: false prop:
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 看起来是这样的:
🌐 Let’s say the original JSX received by List looks like this:
<List>
<Row title="Cabbage" />
<Row title="Garlic" />
<Row title="Apple" />
</List>通过克隆它的子元素,List 可以将额外信息传递给其中的每个 Row。结果如下所示:
🌐 By cloning its children, the List can pass extra information to every Row inside. The result looks like this:
<List>
<Row
title="Cabbage"
isHighlighted={true}
/>
<Row
title="Garlic"
isHighlighted={false}
/>
<Row
title="Apple"
isHighlighted={false}
/>
</List>请注意,按下“下一步”会更新 List 的状态,并高亮不同的行:
🌐 Notice how pressing “Next” updates the state of the List, and highlights a different row:
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 /> 元素,并向它们添加了一个额外的属性。
🌐 To summarize, the List cloned the <Row /> elements it received and added an extra prop to them.
备选方案
🌐 Alternatives
使用渲染属性传递数据
🌐 Passing data with a render prop
与其使用 cloneElement,不如考虑接受一个像 renderItem 这样的渲染属性。在这里,List 将 renderItem 作为属性接收。List 对每个项目调用 renderItem 并传递 isHighlighted 作为参数:
🌐 Instead of using cloneElement, consider accepting a render prop like renderItem. Here, List receives renderItem as a prop. List calls renderItem for every item and passes isHighlighted as an argument:
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 属性被称为“渲染 prop”,因为它是一个指定如何渲染某些内容的 prop。例如,你可以传递一个 renderItem 实现,它使用给定的 isHighlighted 值渲染一个 <Row>:
🌐 The renderItem prop is called a “render prop” because it’s a prop that specifies how to render something. For example, you can pass a renderItem implementation that renders a <Row> with the given isHighlighted value:
<List
items={products}
renderItem={(product, isHighlighted) =>
<Row
key={product.id}
title={product.title}
isHighlighted={isHighlighted}
/>
}
/>最终结果与使用 cloneElement 的结果相同:
🌐 The end result is the same as with cloneElement:
<List>
<Row
title="Cabbage"
isHighlighted={true}
/>
<Row
title="Garlic"
isHighlighted={false}
/>
<Row
title="Apple"
isHighlighted={false}
/>
</List>然而,你可以清楚地追踪到 isHighlighted 值的来源。
🌐 However, you can clearly trace where the isHighlighted value is coming from.
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 更受欢迎,因为它更明确。
🌐 This pattern is preferred to cloneElement because it is more explicit.
通过上下文传递数据
🌐 Passing data through context
另一个替代 cloneElement 的方法是 通过上下文传递数据。
🌐 Another alternative to cloneElement is to pass data through context.
例如,你可以调用 createContext 来定义一个 HighlightContext:
🌐 For example, you can call createContext to define a HighlightContext:
export const HighlightContext = createContext(false);你的 List 组件可以将它渲染的每个项目封装到一个 HighlightContext 提供者中:
🌐 Your List component can wrap every item it renders into a HighlightContext provider:
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 key={item.id} value={isHighlighted}>
{renderItem(item)}
</HighlightContext>
);
})}通过这种方法,Row 根本不需要接收 isHighlighted 属性。相反,它会读取上下文:
🌐 With this approach, Row does not need to receive an isHighlighted prop at all. Instead, it reads the context:
export default function Row({ title }) {
const isHighlighted = useContext(HighlightContext);
// ...这允许调用组件不需要了解或担心将 isHighlighted 传递给 <Row>:
🌐 This allows the calling component to not know or worry about passing isHighlighted to <Row>:
<List
items={products}
renderItem={product =>
<Row title={product.title} />
}
/>相反,List 和 Row 通过上下文协调高亮逻辑。
🌐 Instead, List and Row coordinate the highlighting logic through 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 key={item.id} value={isHighlighted} > {renderItem(item)} </HighlightContext> ); })} <hr /> <button onClick={() => { setSelectedIndex(i => (i + 1) % items.length ); }}> Next </button> </div> ); }
将逻辑提取到自定义钩子中
🌐 Extracting logic into a custom Hook
你可以尝试的另一种方法是将“非视觉”逻辑提取到你自己的 Hook 中,并使用你的 Hook 返回的信息来决定渲染内容。例如,你可以编写一个 useList 自定义 Hook,如下所示:
🌐 Another approach you can try is to extract the “non-visual” logic into your own Hook, and use the information returned by your Hook to decide what to render. For example, you could write a useList custom Hook like this:
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];
}然后你可以像这样使用它:
🌐 Then you could use it like this:
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 中,你可以从任何组件中使用它:
🌐 The data flow is explicit, but the state is inside the useList custom Hook that you can use from any component:
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> ); }
如果你想在不同组件之间复用此逻辑,这种方法尤其有用。
🌐 This approach is particularly useful if you want to reuse this logic between different components.