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ố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ọicreateElement
, hoặc kết quả của một lệnh gọicloneElement
khác. -
props
: Đối sốprops
phả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.props
ban đầu. Nếu không, đối với mỗi prop trong objectprops
, 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ềnprops.key
hoặ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
,true
vàfalse
) 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ôngelement.props
vớiprops
ghi đè mà bạn đã truyền.ref
:element.ref
ban đầu, trừ khi nó bị ghi đè bởiprops.ref
.key
:element.key
ban đầ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
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ếukey
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.
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.