cloneElement
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 props và children 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>Tham số
-
element: Đối sốelementphả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ọicreateElement, hoặc kết quả của một lệnh gọicloneElementkhác. -
props: Đối sốpropsphải là một object hoặcnull. Nếu bạn truyềnnull, element được clone sẽ giữ lại tất cả cácelement.propsban đầu. Nếu không, đối với mỗi prop trong objectprops, element trả về sẽ “ưu tiên” giá trị từpropshơn giá trị từelement.props. Các prop còn lại sẽ được lấy từelement.propsban đầu. Nếu bạn truyềnprops.keyhoặcprops.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,truevàfalse) và mảng các React node. Nếu bạn không truyền bất kỳ đối số...childrennào,element.props.childrenban đầ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ôngelement.propsvớipropsghi đè mà bạn đã truyền.ref:element.refban đầu, trừ khi nó bị ghi đè bởiprops.ref.key:element.keyban đầu, trừ khi nó bị ghi đè bởiprops.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
cloneElementnế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ếukeycho 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. -
cloneElementlà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.
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 cloneElement là truyề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 đó, List và Row 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.