Spec-Zone.ru › React 18

cloneElement

Особенность

Использование cloneElement нечасто встречается и может привести к хрупкому коду. См. распространённые альтернативы.

cloneElement позволяет создать новый элемент React, используя другой элемент в качестве отправной точки.

const clonedElement = cloneElement(element, props, ...children)
  • Ссылка
    • cloneElement(element, props, ...children)
  • Использование
    • Переопределение свойств элемента
  • Альтернативы
    • Передача данных с помощью render-пропа
    • Передача данных через контекст
    • Вынесение логики в пользовательский хук

Ссылка

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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API