Chú Ý

Việc sử dụng cloneElement là không phổ biến và có thể dẫn đến code dễ bị lỗi. Xem các lựa chọn thay thế phổ biến.

cloneElement cho phép bạn tạo một React element mới bằng cách sử dụng một element khác làm điểm bắt đầu.

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

Tham khảo

cloneElement(element, props, ...children)

Gọi cloneElement để tạo một React element dựa trên element, nhưng với propschildren khác:

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>

Xem thêm các ví dụ bên dưới.

Tham số

  • element: Đối số element phải là một React element hợp lệ. Ví dụ: nó có thể là một JSX node như <Something />, kết quả của việc gọi createElement, hoặc kết quả của một lệnh gọi cloneElement khác.

  • props: Đối số props phải là một object hoặc null. Nếu bạn truyền null, element được clone sẽ giữ lại tất cả các element.props ban đầu. Nếu không, đối với mỗi prop trong object props, element trả về sẽ “ưu tiên” giá trị từ props hơn giá trị từ element.props. Các prop còn lại sẽ được lấy từ element.props ban đầu. Nếu bạn truyền props.key hoặc props.ref, chúng sẽ thay thế các giá trị ban đầu.

  • tùy chọn ...children: Không hoặc nhiều child node. Chúng có thể là bất kỳ React node nào, bao gồm React element, string, number, portal, empty node (null, undefined, truefalse) và mảng các React node. Nếu bạn không truyền bất kỳ đối số ...children nào, element.props.children ban đầu sẽ được giữ nguyên.

Giá trị trả về

cloneElement trả về một đối tượng React element với một vài thuộc tính:

  • type: Giống như element.type.
  • props: Kết quả của việc hợp nhất nông element.props với props ghi đè mà bạn đã truyền.
  • ref: element.ref ban đầu, trừ khi nó bị ghi đè bởi props.ref.
  • key: element.key ban đầu, trừ khi nó bị ghi đè bởi props.key.

Thông thường, bạn sẽ trả về element từ component của mình hoặc tạo nó thành một child của một element khác. Mặc dù bạn có thể đọc các thuộc tính của element, nhưng tốt nhất là coi mọi element là không rõ ràng sau khi nó được tạo và chỉ render nó.

Lưu ý

  • Việc clone một element không sửa đổi element ban đầu.

  • Bạn chỉ nên truyền children dưới dạng nhiều đối số cho cloneElement nếu tất cả chúng đều được biết tĩnh, như cloneElement(element, null, child1, child2, child3). Nếu children của bạn là động, hãy truyền toàn bộ mảng làm đối số thứ ba: cloneElement(element, null, listItems). Điều này đảm bảo rằng React sẽ cảnh báo bạn về việc thiếu key cho bất kỳ danh sách động nào. Đối với danh sách tĩnh, điều này là không cần thiết vì chúng không bao giờ sắp xếp lại.

  • cloneElement làm cho việc theo dõi luồng dữ liệu trở nên khó khăn hơn, vì vậy hãy thử các lựa chọn thay thế thay thế.


Cách sử dụng

Ghi đè props của một element

Để ghi đè các props của một React element, hãy truyền nó cho cloneElement với các props bạn muốn ghi đè:

import { cloneElement } from 'react';

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

Ở đây, element được clone kết quả sẽ là <Row title="Cabbage" isHighlighted={true} />.

Hãy xem qua một ví dụ để xem khi nào nó hữu ích.

Hãy tưởng tượng một component List render children của nó dưới dạng một danh sách các hàng có thể chọn với một nút “Next” thay đổi hàng nào được chọn. Component List cần render Row đã chọn khác nhau, vì vậy nó clone mọi child <Row> mà nó đã nhận và thêm một prop isHighlighted: true hoặc isHighlighted: false bổ sung:

export default function List({ children }) {
const [selectedIndex, setSelectedIndex] = useState(0);
return (
<div className="List">
{Children.map(children, (child, index) =>
cloneElement(child, {
isHighlighted: index === selectedIndex
})
)}

Giả sử JSX ban đầu được List nhận trông như thế này:

<List>
<Row title="Cabbage" />
<Row title="Garlic" />
<Row title="Apple" />
</List>

Bằng cách clone children của nó, List có thể truyền thêm thông tin cho mọi Row bên trong. Kết quả trông như thế này:

<List>
<Row
title="Cabbage"
isHighlighted={true}
/>
<Row
title="Garlic"
isHighlighted={false}
/>
<Row
title="Apple"
isHighlighted={false}
/>
</List>

Lưu ý cách nhấn “Next” cập nhật trạng thái của List và làm nổi bật một hàng khác:

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

Tóm lại, List đã clone các element <Row /> mà nó nhận được và thêm một prop bổ sung cho chúng.

Chú Ý

Việc clone children gây khó khăn cho việc biết dữ liệu truyền qua ứng dụng của bạn như thế nào. Hãy thử một trong các lựa chọn thay thế.


Các lựa chọn thay thế

Truyền dữ liệu bằng render prop

Thay vì sử dụng cloneElement, hãy cân nhắc chấp nhận một render prop như renderItem. Ở đây, List nhận renderItem làm một prop. List gọi renderItem cho mỗi item và truyền isHighlighted làm một đối số:

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

Prop renderItem được gọi là “render prop” vì nó là một prop chỉ định cách render một thứ gì đó. Ví dụ: bạn có thể truyền một implementation renderItem render một <Row> với giá trị isHighlighted đã cho:

<List
items={products}
renderItem={(product, isHighlighted) =>
<Row
key={product.id}
title={product.title}
isHighlighted={isHighlighted}
/>
}
/>

Kết quả cuối cùng giống như với cloneElement:

<List>
<Row
title="Cabbage"
isHighlighted={true}
/>
<Row
title="Garlic"
isHighlighted={false}
/>
<Row
title="Apple"
isHighlighted={false}
/>
</List>

Tuy nhiên, bạn có thể theo dõi rõ ràng giá trị isHighlighted đến từ đâu.

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

Mô hình này được ưu tiên hơn cloneElement vì nó rõ ràng hơn.


Truyền dữ liệu qua context

Một lựa chọn thay thế khác cho cloneElementtruyền dữ liệu qua context.

Ví dụ: bạn có thể gọi createContext để xác định HighlightContext:

export const HighlightContext = createContext(false);

Component List của bạn có thể bọc mọi item mà nó render vào một provider 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>
);
})}

Với cách tiếp cận này, Row không cần nhận một prop isHighlighted nào cả. Thay vào đó, nó đọc context:

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

Điều này cho phép component gọi không biết hoặc lo lắng về việc truyền isHighlighted cho <Row>:

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

Thay vào đó, ListRow phối hợp logic làm nổi bật thông qua 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>
  );
}

Tìm hiểu thêm về việc truyền dữ liệu qua context.


Trích xuất logic vào một Hook tùy chỉnh

Một cách tiếp cận khác mà bạn có thể thử là trích xuất logic “phi trực quan” vào Hook của riêng bạn và sử dụng thông tin được trả về bởi Hook của bạn để quyết định những gì cần render. Ví dụ: bạn có thể viết một Hook tùy chỉnh useList như thế này:

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

Sau đó, bạn có thể sử dụng nó như thế này:

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

Luồng dữ liệu là rõ ràng, nhưng trạng thái nằm bên trong Hook tùy chỉnh useList mà bạn có thể sử dụng từ bất kỳ component nào:

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

Cách tiếp cận này đặc biệt hữu ích nếu bạn muốn sử dụng lại logic này giữa các component khác nhau.