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 попередить вас про відсутністьkey
s для будь-яких динамічних списків. Для статичних списків це не потрібно, оскільки вони ніколи не змінюють порядок.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;
}
Цей підхід особливо корисний, якщо ви хочете повторно використовувати цю логіку між різними компонентами.