易犯错误

使用 cloneElement 不常见,并且可能导致代码脆弱。查看常见替代方案。

🌐 Using cloneElement is uncommon and can lead to fragile code. See common alternatives.

cloneElement 让你可以使用另一个元素作为起点来创建一个新的 React 元素。

const clonedElement = cloneElement(element, props, ...children)

参考

🌐 Reference

cloneElement(element, props, ...children)

调用 cloneElement 根据 element 创建一个 React 元素,但使用不同的 propschildren

🌐 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

  • elementelement 参数必须是一个有效的 React 元素。例如,它可以是一个 JSX 节点,如 <Something />,也可以是调用 createElement 的结果,或者是另一个 cloneElement 调用的结果。
  • propsprops 参数必须是对象或 null。如果传入 null,克隆的元素将保留原始 element.props 的所有内容。否则,对于 props 对象中的每个属性,返回的元素将“优先”使用 props 的值,而不是 element.props 的值。其余属性将从原始的 element.props 中填充。如果传入 props.keyprops.ref,它们将替换原始的值。
  • 可选 ...children:零个或多个子节点。它们可以是任何 React 节点,包括 React 元素、字符串、数字、门户、空节点(nullundefinedtruefalse)以及 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: trueisHighlighted: 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.

易犯错误

克隆子组件会让你难以判断数据在应用中的流向。尝试使用其中一个替代方案。

🌐 Cloning children makes it hard to tell how the data flows through your app. Try one of the alternatives.


备选方案

🌐 Alternatives

使用渲染属性传递数据

🌐 Passing data with a render prop

与其使用 cloneElement,不如考虑接受一个像 renderItem 这样的渲染属性。在这里,ListrenderItem 作为属性接收。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} />
}
/>

相反,ListRow 通过上下文协调高亮逻辑。

🌐 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.