易犯错误

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

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

cloneElement 允许你使用另一个元素作为起点创建一个新的 React 元素。

¥cloneElement lets you create a new React element using another element as a starting point.

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>

请参阅下面的更多示例。

¥See more examples below.

参数

¥Parameters

  • elementelement 参数必须是有效的 React 元素。例如,它可以是一个像 <Something /> 这样的 JSX 节点,也可以是调用 createElement 的结果,或者另一个 cloneElement 调用的结果。

    ¥element: The element argument must be a valid React element. For example, it could be a JSX node like <Something />, the result of calling createElement, or the result of another cloneElement call.

  • propsprops 参数必须是对象或 null。如果你传递 null,克隆的元素将保留原始 element.props 的所有内容。否则,对于 props 对象中的每个 prop,返回的元素将 “prefer” props 的值,而不是 element.props 的值。其余属性将由原始 element.props 填充。如果你传递 props.keyprops.ref,它们将替换原始元素。

    ¥props: The props argument must either be an object or null. If you pass null, the cloned element will retain all of the original element.props. Otherwise, for every prop in the props object, the returned element will “prefer” the value from props over the value from element.props. The rest of the props will be filled from the original element.props. If you pass props.key or props.ref, they will replace the original ones.

  • 可选 ...children:零个或多个子节点。它们可以是任何 React 节点,包括 React 元素、字符串、数字、portals、空节点(nullundefinedtruefalse)以及 React 节点数组。如果你不传递任何 ...children 参数,则原始 element.props.children 将被保留。

    ¥optional ...children: Zero or more child nodes. They can be any React nodes, including React elements, strings, numbers, portals, empty nodes (null, undefined, true, and false), and arrays of React nodes. If you don’t pass any ...children arguments, the original element.props.children will be preserved.

返回

¥Returns

cloneElement 返回一个具有少量属性的 React 元素对象:

¥cloneElement returns a React element object with a few properties:

  • type:与 element.type 相同。

    ¥type: Same as element.type.

  • props:将 element.props 与你传递的覆盖 props 浅合并的结果。

    ¥props: The result of shallowly merging element.props with the overriding props you have passed.

  • ref:原始 element.ref,除非它被 props.ref 覆盖。

    ¥ref: The original element.ref, unless it was overridden by props.ref.

  • key:原始 element.key,除非它被 props.key 覆盖。

    ¥key: The original element.key, unless it was overridden by 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

  • 克隆元素不会修改原始元素。

    ¥Cloning an element does not modify the original element.

  • 只有当子组件都是静态已知的(例如 cloneElement(element, null, child1, child2, child3))时,才应将它们作为多个参数传递给 cloneElement。如果你的子组件是动态的,请将整个数组作为第三个参数传递:cloneElement(element, null, listItems)。这确保 React 将对任何动态列表进行 警告你缺少 key。对于静态列表,这不是必需的,因为它们永远不会重新排序。

    ¥You should only pass children as multiple arguments to cloneElement if they are all statically known, like cloneElement(element, null, child1, child2, child3). If your children are dynamic, pass the entire array as the third argument: cloneElement(element, null, listItems). This ensures that React will warn you about missing keys for any dynamic lists. For static lists this is not necessary because they never reorder.

  • cloneElement 使得追踪数据流变得更加困难,因此请尝试使用 alternatives

    ¥cloneElement makes it harder to trace the data flow, so try the alternatives instead.


用法

¥Usage

覆盖元素

¥Overriding props of an element

要覆盖某些 React 元素 的属性,请将其与要覆盖的 props 一起传递给 cloneElement

¥To override the props of some React element, pass it to cloneElement with the props you want to override:

import { cloneElement } from 'react';

// ...
const clonedElement = cloneElement(
<Row title="Cabbage" />,
{ isHighlighted: true }
);

此处,生成的 克隆元素 将是 <Row title="Cabbage" isHighlighted={true} />

¥Here, the resulting cloned element will be <Row title="Cabbage" isHighlighted={true} />.

让我们通过一个例子来了解它何时有用。

¥Let’s walk through an example to see when it’s useful.

想象一下,一个 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 /> 元素,并为其添加了一个额外的 prop。

¥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 这样的渲染 prop。这里,List 接收 renderItem 作为 prop。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 属性之所以被称为 “render 属性”,是因为它是一个指定如何渲染内容的属性。例如,你可以传递一个 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.Provider key={item.id} value={isHighlighted}>
{renderItem(item)}
</HighlightContext.Provider>
);
})}

通过这种方法,Row 根本不需要接收 isHighlighted prop。它会读取 context:

¥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 通过 context 协调高亮逻辑。

¥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.Provider
            key={item.id}
            value={isHighlighted}
          >
            {renderItem(item)}
          </HighlightContext.Provider>
        );
      })}
      <hr />
      <button onClick={() => {
        setSelectedIndex(i =>
          (i + 1) % items.length
        );
      }}>
        Next
      </button>
    </div>
  );
}

了解更多关于通过上下文传递数据的信息。

¥Learn more about passing data through context.


将逻辑提取到自定义钩子中

¥Extracting logic into a custom Hook

你可以尝试的另一种方法是将 “non-visual” 逻辑提取到你自己的钩子中,并使用钩子返回的信息来决定要渲染的内容。例如,你可以像这样编写一个 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 自定义钩子中,你可以从任何组件中使用它:

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