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 з перевизначенням пропсів, які ви передали.
  • ref: Оригінальний element.ref, якщо його не було перевизначено props.ref.
  • ключ: Оригінальний element.key, якщо його не було перевизначено props.key.

Зазвичай, ви повертаєте елемент зі свого компонента або робите його дочірнім елементом іншого елемента. Хоча ви можете прочитати властивості елемента, найкраще вважати кожен елемент непрозорим після його створення і лише відрендерити його.

Застереження

  • Клонування елемента не змінює оригінальний елемент.

  • Вам слід передавати дочірні елементи як декілька аргументів до cloneElement, якщо всі вони є статично відомими, наприклад cloneElement(element, null, child1, child2, child3). Якщо ваші дочірні елементи динамічні, передайте весь масив як третій аргумент: cloneElement(element, null, listItems). Це гарантує, що React попередить вас про відсутність keys для будь-яких динамічних списків. Для статичних списків це не потрібно, оскільки вони ніколи не змінюють порядок.

  • cloneElement ускладнює відстеження потоку даних, тому спробуйте замість цього альтернативи .


Використання

Перевизначення пропсів елемента

Перевизначення пропсів деякого React-елемента</CodeStep> , передайте його до <code>cloneElement з параметрами , які ви хочете перевизначити</CodeStep> :</p> <pre><code data-meta="[[1, 5, "<Row title=\"Cabbage\" />"], [2, 6, "{ isHighlighted: true }"], [3, 4, "clonedElement"]]" class="language-js">import { cloneElement } from 'react'; // ... const clonedElement = cloneElement( <Row title="Cabbage" />, { isHighlighted: true } );

Тут отриманий клонований елемент </CodeStep> буде <код><Row title="Капуста" isHighlighted={true} />.

Давайте розглянемо приклад, щоб побачити, коли це може бути корисним.

Уявіть собі компонент 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 
        })
      )}

Припустимо, оригінальний 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 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;
}

Цей підхід особливо корисний, якщо ви хочете повторно використовувати цю логіку між різними компонентами.