cloneElement
Особенность
Использование cloneElement нечасто встречается и может привести к хрупкому коду. См. распространённые альтернативы.
cloneElement позволяет создать новый элемент React, используя другой элемент в качестве отправной точки.
const clonedElement = cloneElement(element, props, ...children)
Ссылка
cloneElement(element, props, ...children)
Вызовите cloneElement для создания элемента React, основанного на element, но с другими props и children:
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.keyилиprops.ref, они заменят исходные. -
необязательный
...children: Ноль или более дочерних узлов. Они могут быть любыми узлами React, включая элементы React, строки, числа, порталы, пустые узлы (null,undefined,true, иfalse), и массивы узлов 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: true или isHighlighted: 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
})
)} Предположим, исходный JSX, полученный компонентом List, выглядит так:
<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 /> элементы, которые он получил, и добавил к ним дополнительное свойство.
Особенность
Клонирование дочерних элементов затрудняет отслеживание того, как данные проходят через ваше приложение. Попробуйте одну из альтернатив.
Альтернативы
Передача данных с помощью render-пропа
Вместо использования cloneElement, рассмотрите возможность принятия render-пропа, такого как 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 называется «render-пропом», потому что это свойство, которое определяет, как что-то отобразить. Например, вы можете передать реализацию renderItem, которая отображает <Row> со значением isHighlighted:
<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} />
}
/> Вместо этого List и Row координируют логику выделения через контекст.
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> ); }
Узнайте больше о передаче данных через контекст.
Вынесение логики в пользовательский хук
Ещё один подход, который вы можете попробовать, - вынести «невизуальную» логику в свой собственный хук и использовать информацию, возвращаемую вашим хуком, чтобы решить, что отображать. Например, вы можете написать пользовательский хук useList так:
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, который вы можете использовать из любого компонента:
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> ); }
Этот подход особенно полезен, если вы хотите повторно использовать эту логику между различными компонентами.
© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://react.dev/reference/react/cloneElement