陷阱

使用 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>

請參閱下面的更多範例。

參數

  • elementelement 參數必須是有效的 React 元素。例如,它可以是像 <Something /> 這樣的 JSX 節點,呼叫 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 將會被保留。

回傳

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 的狀態,並 highlight 不同的列

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 /> 元素,並為它們添加了一個額外的屬性。

陷阱

複製子元素會讓您難以判斷資料如何在應用程式中流動。嘗試以下其中一種替代方案


替代方案

使用渲染屬性傳遞資料

考慮使用像 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 更受歡迎,因為它更明確。


透過 Context 傳遞資料

cloneElement 的另一個替代方案是透過 Context 傳遞資料

例如,您可以呼叫 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 屬性。相反,它會讀取 Context。

export default function Row({ title }) {
const isHighlighted = useContext(HighlightContext);
// ...

這允許呼叫元件不必知道或擔心將 isHighlighted 傳遞給 <Row>

<List
items={products}
renderItem={product =>
<Row title={product.title} />
}
/>

相反,ListRow 透過 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>
  );
}

深入瞭解透過 Context 傳遞資料。


將邏輯提取到自訂 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>
  );
}

如果您想在不同的元件之間重複使用此邏輯,這種方法特別有用。