cloneElement
Використання cloneElement є рідкісним і може призвести до нестійкого коду. Дивіться типові альтернативи.
cloneElement дозволяє створити новий елемент React, використовуючи інший елемент як відправну точку.
const clonedElement = cloneElement(element, props, ...children)
Довідник
cloneElement(element, props, ...children)
Виклик cloneElement для створення React-елемента на основі елемента, але з іншими 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-елемента з декількома властивостями:
тип: Те саме, що йelement.type.props: Результат неглибокого злиттяelement.propsз перевизначеннямпропсів, які ви передали.ключ: Оригінальнийelement.key, якщо його не було перевизначеноprops.key.
element.ref, якщо його не було перевизначено props.ref.
Зазвичай, ви повертаєте елемент зі свого компонента або робите його дочірнім елементом іншого елемента. Хоча ви можете прочитати властивості елемента, найкраще вважати кожен елемент непрозорим після його створення і лише відрендерити його.
Застереження
Клонування елемента не змінює оригінальний елемент.
Вам слід передавати дочірні елементи як декілька аргументів до
cloneElement, якщо всі вони є статично відомими, наприкладcloneElement(element, null, child1, child2, child3). Якщо ваші дочірні елементи динамічні, передайте весь масив як третій аргумент:cloneElement(element, null, listItems). Це гарантує, що React попередить вас про відсутністьkeys для будь-яких динамічних списків. Для статичних списків це не потрібно, оскільки вони ніколи не змінюють порядок.cloneElementускладнює відстеження потоку даних, тому спробуйте замість цього альтернативи .
Використання
Перевизначення пропсів елемента
Перевизначення пропсів деякого Тут отриманий Давайте розглянемо приклад, щоб побачити, коли це може бути корисним. Уявіть собі компонент Припустимо, оригінальний JSX, отриманий Клонуючи своїх нащадків, Помітьте, як натискання кнопки "Далі" оновлює стан List, який виводить свої дочірні елементи у вигляді списку рядків, які можна вибрати, з кнопкою "Далі", яка змінює вибір рядка. Компонент 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
})
)}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 List from './List.js';
import Row from './Row.js';
import { products } from './data.js';
export default function App() {
return (
<List>
{products.map(product =>
<Row
key={product.id}
title={product.title}
/>
)}
</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>
);
}
export default function Row({ title, isHighlighted }) {
return (
<div className={[
'Row',
isHighlighted ? 'RowHighlighted' : ''
].join(' ')}>
{title}
</div>
);
}
export const products = [
{ title: 'Cabbage', id: 1 },
{ title: 'Garlic', id: 2 },
{ title: 'Apple', id: 3 },
];
.List {
display: flex;
flex-direction: column;
border: 2px solid grey;
padding: 5px;
}
.Row {
border: 2px dashed black;
padding: 5px;
margin: 5px;
}
.RowHighlighted {
background: #ffa;
}
button {
height: 40px;
font-size: 20px;
}
Підсумовуючи, List клонував отримані елементи <Row /> і додав до них додатковий проп.
Клонування дочірніх хуків ускладнює визначення того, як дані проходять через ваш застосунок. Спробуйте одну з альтернатив.
Альтернативи
Передача даних за допомогою пропу рендерингу
Замість використання cloneElement, розгляньте можливість прийняття пропперу на зразок 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 називається "проп рендерингу", оскільки він вказує, як щось відрендерити. Наприклад, ви можете передати реалізацію 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 List from './List.js';
import Row from './Row.js';
import { products } from './data.js';
export default function App() {
return (
<List
items={products}
renderItem={(product, isHighlighted) =>
<Row
key={product.id}
title={product.title}
isHighlighted={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>
);
}
export default function Row({ title, isHighlighted }) {
return (
<div className={[
'Row',
isHighlighted ? 'RowHighlighted' : ''
].join(' ')}>
{title}
</div>
);
}
export const products = [
{ title: 'Cabbage', id: 1 },
{ title: 'Garlic', id: 2 },
{ title: 'Apple', id: 3 },
];
.List {
display: flex;
flex-direction: column;
border: 2px solid grey;
padding: 5px;
}
.Row {
border: 2px dashed black;
padding: 5px;
margin: 5px;
}
.RowHighlighted {
background: #ffa;
}
button {
height: 40px;
font-size: 20px;
}
Цьому шаблону надається перевага перед 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 List from './List.js';
import Row from './Row.js';
import { products } from './data.js';
export default function App() {
return (
<List
items={products}
renderItem={(product) =>
<Row title={product.title} />
}
/>
);
}
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>
);
}
import { useContext } from 'react';
import { HighlightContext } from './HighlightContext.js';
export default function Row({ title }) {
const isHighlighted = useContext(HighlightContext);
return (
<div className={[
'Row',
isHighlighted ? 'RowHighlighted' : ''
].join(' ')}>
{title}
</div>
);
}
import { createContext } from 'react';
export const HighlightContext = createContext(false);
export const products = [
{ title: 'Cabbage', id: 1 },
{ title: 'Garlic', id: 2 },
{ title: 'Apple', id: 3 },
];
.List {
display: flex;
flex-direction: column;
border: 2px solid grey;
padding: 5px;
}
.Row {
border: 2px dashed black;
padding: 5px;
margin: 5px;
}
.RowHighlighted {
background: #ffa;
}
button {
height: 40px;
font-size: 20px;
}
Дізнайтеся більше про передачу даних через контекст.
Винесення логіки у користувацький хук
Ще один підхід, який ви можете спробувати, - це витягнути "невізуальну" логіку у власний хук і використовувати інформацію, повернуту хуком, щоб вирішити, що рендерити. Наприклад, ви можете написати 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>
);
}
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 Row({ title, isHighlighted }) {
return (
<div className={[
'Row',
isHighlighted ? 'RowHighlighted' : ''
].join(' ')}>
{title}
</div>
);
}
export const products = [
{ title: 'Cabbage', id: 1 },
{ title: 'Garlic', id: 2 },
{ title: 'Apple', id: 3 },
];
.List {
display: flex;
flex-direction: column;
border: 2px solid grey;
padding: 5px;
}
.Row {
border: 2px dashed black;
padding: 5px;
margin: 5px;
}
.RowHighlighted {
background: #ffa;
}
button {
height: 40px;
font-size: 20px;
}
Цей підхід особливо корисний, якщо ви хочете повторно використовувати цю логіку між різними компонентами.