陷阱

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

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

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

参考

cloneElement(element, props, ...children)

调用 cloneElement 基于 element 创建一个 React 元素,但是具有不同的 propschildren

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.keyprops.ref,它们将替换原始的那些。

  • 可选 ...children:零个或多个子节点。它们可以是任何 React 节点,包括 React 元素、字符串、数字、Portals、空节点(nullundefinedtruefalse)以及 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: trueisHighlighted: 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 /> 元素,并向其添加了一个额外的属性。

陷阱

克隆子组件使得难以追踪数据在应用中的流动路径。请尝试使用以下替代方案。


替代方案

使用渲染属性传递数据

不要使用cloneElement,考虑接受一个类似于renderItem渲染属性。在这里,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} />
}
/>

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

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>
  );
}

如果你想在不同的组件之间重用此逻辑,这种方法特别有用。