Children

Використання Children є рідкісним і може призвести до нестійкого коду. Дивіться типові альтернативи.

Children дозволяє маніпулювати та трансформувати JSX, який ви отримали як children prop.

const mappedChildren = Children.map(children, child =>
  <div className="Row">
    {child}
  </div>
);

Довідник

Children.count(children)

Виклик Children.count(children) для підрахунку кількості дітей у структурі даних children.

import { Children } from 'react';

function RowList({ children }) {
  return (
    <>
      <h1>Total rows: {Children.count(children)}</h1>
      ...
    </>
  );
}

Дивіться більше прикладів нижче.

Параметри

  • children: значення children prop, отримане вашим компонентом.

Повернення

Кількість вузлів всередині цих нащадків.

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

  • Порожні вузли (null, undefined та булеві), рядки, числа та React-елементи вважаються окремими вузлами. Масиви не вважаються окремими вузлами, але їхні дочірні елементи вважаються. Обхід не йде глибше React-елементів: вони не рендеряться, а їхні дочірні елементи не обходяться. Фрагменти не обходять.

Children.forEach(children, fn, thisArg?)

Виклик Children.forEach(children, fn, thisArg?) для виконання коду для кожного дочірнього елемента структури даних children.

import { Children } from 'react';

function SeparatorList({ children }) {
  const result = [];
  Children.forEach(children, (child, index) => {
    result.push(child);
    result.push(<hr key={index} />);
  });
  // ...

Дивіться більше прикладів нижче.

Параметри

  • children: значення children prop, отримане вашим компонентом.
  • fn
    : Функція, яку ви хочете запустити для кожного дочірнього ефекту, подібно до масиву forEach методу зворотного виклику. Він буде викликаний з дочірнім елементом як першим аргументом і його індексом як другим аргументом. Індекс починається з 0 і збільшується при кожному виклику.
  • опція thisArg: це значення, з яким слід викликати функцію fn. Якщо його опустити, це буде undefined.

Повертає

Children.forEach повертає undefined.

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

  • Порожні вузли (null, undefined та булеві), рядки, числа та React-елементи вважаються окремими вузлами. Масиви не вважаються окремими вузлами, але їхні дочірні елементи вважаються. Обхід не йде глибше React-елементів: вони не рендеряться, а їхні дочірні елементи не обходяться. Фрагменти не обходять.

Children.map(children, fn, thisArg?)

Виклик Children.map(children, fn, thisArg?) для відображення або перетворення кожного дочірнього елемента структури даних children.

import { Children } from 'react';

function RowList({ children }) {
  return (
    <div className="RowList">
      {Children.map(children, child =>
        <div className="Row">
          {child}
        </div>
      )}
    </div>
  );
}

Дивіться більше прикладів нижче.

Параметри

  • children: значення children prop, отримане вашим компонентом.
  • fn: Функція відображення, подібна до масиву map методу зворотного виклику. Він буде викликаний з дочірнім елементом як першим аргументом і його індексом як другим аргументом. Індекс починається з 0 і збільшується при кожному виклику. Вам потрібно повернути вузол React з цієї функції. Це може бути порожній вузол (null, undefined або булевий), рядок, число, елемент React або масив інших вузлів React.
  • опція thisArg: це значення, з яким слід викликати функцію fn. Якщо його опустити, це буде undefined.

Повернення

Якщо children дорівнює null або undefined, повертає те саме значення.

Інакше повертає плаский масив, що складається з вузлів, які ви повернули з функції fn. Повернутий масив міститиме всі вузли, крім null та undefined.

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

  • Порожні вузли (null, undefined та булеві), рядки, числа та React-елементи вважаються окремими вузлами. Масиви не вважаються окремими вузлами, але їхні дочірні елементи вважаються. Обхід не йде глибше React-елементів: вони не рендеряться, а їхні дочірні елементи не обходяться. Фрагменти не обходять.

  • Якщо повернути елемент або масив елементів з ключами з fn, ключі повернених елементів буде автоматично об'єднано з ключем відповідного вихідного елемента з children. Якщо ви повертаєте декілька елементів з fn у масиві, їхні ключі мають бути унікальними лише локально між собою.


Children.only(children)

Викликати Children.only(children), щоб стверджувати, що дочірні елементи представляють один елемент React.

function Box({ children }) {
  const element = Children.only(children);
  // ...

Параметри

  • children: значення children prop, отримане вашим компонентом.

Повернення

Якщо children є допустимим елементом, повертає цей елемент.

Інакше - помилка.

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

  • Цей метод завжди виникає, якщо ви передаєте масив (наприклад, значення, що повертається Children.map) як дітей . Іншими словами, він забезпечує, щоб children був одним елементом React, а не масивом з одним елементом.

Children.toArray(children)

Виклик Children.toArray(children) для створення масиву зі структури даних children.

import { Children } from 'react';

export default function ReversedList({ children }) {
  const result = Children.toArray(children);
  result.reverse();
  // ...

Параметри

  • children: значення children prop, отримане вашим компонентом.

Повернення

Повертає плаский масив елементів у children.

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

  • Порожні вузли (null, undefined та булеві) буде пропущено у повернутому масиві. Ключі повернених елементів буде обчислено на основі ключів початкових елементів та їхнього рівня вкладеності і позиції. Це гарантує, що сплощення масиву не призведе до змін у поведінці.

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

Перетворення дочірніх елементів

Щоб перетворити дочірні JSX, які ваш компонент отримує як проп children, викличте Children.map:

import { Children } from 'react';

function RowList({ children }) {
  return (
    <div className="RowList">
      {Children.map(children, child =>
        <div className="Row">
          {child}
        </div>
      )}
    </div>
  );
}

У наведеному вище прикладі RowList обгортає кожний отриманий дочірній елемент у контейнер <div className="Row">. Наприклад, скажімо, батьківський компонент передає три теги <p> як проп children до RowList:

<RowList>
  <p>This is the first item.</p>
  <p>This is the second item.</p>
  <p>This is the third item.</p>
</RowList>

Тоді, з реалізацією RowList, наведеною вище, кінцевий результат візуалізації матиме такий вигляд:

<div className="RowList">
  <div className="Row">
    <p>This is the first item.</p>
  </div>
  <div className="Row">
    <p>This is the second item.</p>
  </div>
  <div className="Row">
    <p>This is the third item.</p>
  </div>
</div>

Children.map подібно до перетворення масивів за допомогою map(). Різниця полягає у тому, що структура даних children вважається непрозорою. Це означає, що навіть якщо вона іноді є масивом, ви не повинні вважати, що це масив або будь-який інший конкретний тип даних. Ось чому вам слід використовувати Children.map, якщо вам потрібно його перетворити.

import RowList from './RowList.js';

export default function App() {
  return (
    <RowList>
      <p>This is the first item.</p>
      <p>This is the second item.</p>
      <p>This is the third item.</p>
    </RowList>
  );
}
import { Children } from 'react';

export default function RowList({ children }) {
  return (
    <div className="RowList">
      {Children.map(children, child =>
        <div className="Row">
          {child}
        </div>
      )}
    </div>
  );
}
.RowList {
  display: flex;
  flex-direction: column;
  border: 2px solid grey;
  padding: 5px;
}

.Row {
  border: 2px dashed black;
  padding: 5px;
  margin: 5px;
}

Чому дочірній проп не завжди є масивом?

У React проп children вважається непрозорою структурою даних. Це означає, що ви не повинні покладатися на те, як він структурований. Для перетворення, фільтрації або підрахунку дочірніх елементів слід використовувати методи Children.

На практиці структура даних children часто представляється як масив всередині. Однак, якщо є лише один дочірній елемент, то React не буде створювати додатковий масив, оскільки це призведе до зайвих витрат пам'яті. Поки ви використовуєте методи Children замість того, щоб безпосередньо інтроспектувати проп children, ваш код не зламається, навіть якщо React змінить те, як насправді реалізована структура даних.

Навіть коли діти є масивом, Children.map має корисну спеціальну поведінку. Наприклад, Children.map об'єднує ключі у повернених елементах з ключами у children, які ви йому передали. Це гарантує, що оригінальні JSX-нащадки не "втратять" ключі, навіть якщо їх буде обгорнуто, як у наведеному вище прикладі.

Структура даних children не включає відрендерене виведення компонентів, які ви передаєте як JSX. У наведеному нижче прикладі children, отриманий RowList, містить лише два елементи замість трьох:

  1. <p>This is the first item.</p>
  2. <MoreRows />

Тому у цьому прикладі згенеровано лише дві обгортки рядків:

import RowList from './RowList.js';

export default function App() {
  return (
    <RowList>
      <p>This is the first item.</p>
      <MoreRows />
    </RowList>
  );
}

function MoreRows() {
  return (
    <>
      <p>This is the second item.</p>
      <p>This is the third item.</p>
    </>
  );
}
import { Children } from 'react';

export default function RowList({ children }) {
  return (
    <div className="RowList">
      {Children.map(children, child =>
        <div className="Row">
          {child}
        </div>
      )}
    </div>
  );
}
.RowList {
  display: flex;
  flex-direction: column;
  border: 2px solid grey;
  padding: 5px;
}

.Row {
  border: 2px dashed black;
  padding: 5px;
  margin: 5px;
}

Не існує способу отримати відрендерене виведення внутрішнього компонента як <MoreRows /> при маніпулюванні дочірніми компонентами. Ось чому зазвичай краще скористатися одним з альтернативних рішень.


Запуск деякого коду для кожного дочірнього елемента

Виклик Children.forEach для перебору кожного нащадка у структурі даних children. Він не повертає жодного значення і подібний до методу arrayforEach. Ви можете використовувати його для виконання власної логіки, наприклад, побудови власного масиву.

import SeparatorList from './SeparatorList.js';

export default function App() {
  return (
    <SeparatorList>
      <p>This is the first item.</p>
      <p>This is the second item.</p>
      <p>This is the third item.</p>
    </SeparatorList>
  );
}
import { Children } from 'react';

export default function SeparatorList({ children }) {
  const result = [];
  Children.forEach(children, (child, index) => {
    result.push(child);
    result.push(<hr key={index} />);
  });
  result.pop(); // Remove the last separator
  return result;
}

Як згадувалося раніше, не існує способу отримати відрендерене виведення внутрішнього компонента при маніпуляціях з дочірніми компонентами. Тому зазвичай краще скористатися одним з альтернативних рішень.


Підрахунок дочірніх елементів

Виклик Children.count(children) для підрахунку кількості дітей.

import RowList from './RowList.js';

export default function App() {
  return (
    <RowList>
      <p>This is the first item.</p>
      <p>This is the second item.</p>
      <p>This is the third item.</p>
    </RowList>
  );
}
import { Children } from 'react';

export default function RowList({ children }) {
  return (
    <div className="RowList">
      <h1 className="RowListHeader">
        Total rows: {Children.count(children)}
      </h1>
      {Children.map(children, child =>
        <div className="Row">
          {child}
        </div>
      )}
    </div>
  );
}
.RowList {
  display: flex;
  flex-direction: column;
  border: 2px solid grey;
  padding: 5px;
}

.RowListHeader {
  padding-top: 5px;
  font-size: 25px;
  font-weight: bold;
  text-align: center;
}

.Row {
  border: 2px dashed black;
  padding: 5px;
  margin: 5px;
}

Як згадувалося раніше, не існує способу отримати відрендерене виведення внутрішнього компонента при маніпуляціях з дочірніми компонентами. Тому зазвичай краще скористатися одним з альтернативних рішень.


Перетворення дочірніх елементів у масив

Викличте Children.toArray(children), щоб перетворити структуру дочірніх даних на звичайний масив JavaScript. Це дозволить вам маніпулювати масивом за допомогою вбудованих методів масиву, таких як filter, sort або reverse.

import ReversedList from './ReversedList.js';

export default function App() {
  return (
    <ReversedList>
      <p>This is the first item.</p>
      <p>This is the second item.</p>
      <p>This is the third item.</p>
    </ReversedList>
  );
}
import { Children } from 'react';

export default function ReversedList({ children }) {
  const result = Children.toArray(children);
  result.reverse();
  return result;
}

Як згадувалося раніше, не існує способу отримати відрендерене виведення внутрішнього компонента при маніпуляціях з дочірніми компонентами. Тому зазвичай краще скористатися одним з альтернативних рішень.


Альтернативи

У цьому розділі описано альтернативи API Children (з великої літери C), який імпортується так:

import { Children } from 'react';

Не плутайте це з використанням дочірнього пропу (маленька літера c), що є гарною та рекомендованою практикою.

Експонування декількох компонентів

Маніпуляції з дочірніми елементами за допомогою методів Children часто призводять до нестійкого коду. Коли ви передаєте дочірні елементи компоненту у JSX, ви зазвичай не очікуєте, що компонент буде маніпулювати або перетворювати окремі дочірні елементи.

За можливості намагайтеся уникати використання методів Children. Наприклад, якщо ви хочете, щоб кожен нащадок RowList було загорнуто у <div className="Row">, експортуйте компонент Row і вручну загорніть у нього кожен рядок так:

import { RowList, Row } from './RowList.js';

export default function App() {
  return (
    <RowList>
      <Row>
        <p>This is the first item.</p>
      </Row>
      <Row>
        <p>This is the second item.</p>
      </Row>
      <Row>
        <p>This is the third item.</p>
      </Row>
    </RowList>
  );
}
export function RowList({ children }) {
  return (
    <div className="RowList">
      {children}
    </div>
  );
}

export function Row({ children }) {
  return (
    <div className="Row">
      {children}
    </div>
  );
}
.RowList {
  display: flex;
  flex-direction: column;
  border: 2px solid grey;
  padding: 5px;
}

.Row {
  border: 2px dashed black;
  padding: 5px;
  margin: 5px;
}

На відміну від використання Children.map, цей підхід не обгортає кожний дочірній елемент автоматично. Втім, цей підхід має значну перевагу порівняно з попереднім прикладом з Children.map, оскільки він працює, навіть якщо ви продовжуєте видобувати нові компоненти. Наприклад, він все ще працює, якщо ви видобуваєте власний MoreRows компонент:

import { RowList, Row } from './RowList.js';

export default function App() {
  return (
    <RowList>
      <Row>
        <p>This is the first item.</p>
      </Row>
      <MoreRows />
    </RowList>
  );
}

function MoreRows() {
  return (
    <>
      <Row>
        <p>This is the second item.</p>
      </Row>
      <Row>
        <p>This is the third item.</p>
      </Row>
    </>
  );
}
export function RowList({ children }) {
  return (
    <div className="RowList">
      {children}
    </div>
  );
}

export function Row({ children }) {
  return (
    <div className="Row">
      {children}
    </div>
  );
}
.RowList {
  display: flex;
  flex-direction: column;
  border: 2px solid grey;
  padding: 5px;
}

.Row {
  border: 2px dashed black;
  padding: 5px;
  margin: 5px;
}

Це не спрацює з Children.map, оскільки він "бачитиме" <MoreRows /> як єдиний дочірній елемент (і єдиний рядок).


Прийняття масиву об'єктів як пропу

Ви також можете явно передати масив як проп. Наприклад, цей RowList приймає масив rows як проп:

import { RowList, Row } from './RowList.js';

export default function App() {
  return (
    <RowList rows={[
      { id: 'first', content: <p>This is the first item.</p> },
      { id: 'second', content: <p>This is the second item.</p> },
      { id: 'third', content: <p>This is the third item.</p> }
    ]} />
  );
}
export function RowList({ rows }) {
  return (
    <div className="RowList">
      {rows.map(row => (
        <div className="Row" key={row.id}>
          {row.content}
        </div>
      ))}
    </div>
  );
}
.RowList {
  display: flex;
  flex-direction: column;
  border: 2px solid grey;
  padding: 5px;
}

.Row {
  border: 2px dashed black;
  padding: 5px;
  margin: 5px;
}

Оскільки rows є звичайним масивом JavaScript, компонент RowList може використовувати вбудовані методи масиву, такі як map на ньому.

Цей шаблон особливо корисний, коли ви хочете мати можливість передавати більше інформації у вигляді структурованих даних разом з дочірніми елементами. У наведеному нижче прикладі компонент TabSwitcher отримує масив об'єктів як проп tabs:

import TabSwitcher from './TabSwitcher.js';

export default function App() {
  return (
    <TabSwitcher tabs={[
      {
        id: 'first',
        header: 'First',
        content: <p>This is the first item.</p>
      },
      {
        id: 'second',
        header: 'Second',
        content: <p>This is the second item.</p>
      },
      {
        id: 'third',
        header: 'Third',
        content: <p>This is the third item.</p>
      }
    ]} />
  );
}
import { useState } from 'react';

export default function TabSwitcher({ tabs }) {
  const [selectedId, setSelectedId] = useState(tabs[0].id);
  const selectedTab = tabs.find(tab => tab.id === selectedId);
  return (
    <>
      {tabs.map(tab => (
        <button
          key={tab.id}
          onClick={() => setSelectedId(tab.id)}
        >
          {tab.header}
        </button>
      ))}
      <hr />
      <div key={selectedId}>
        <h3>{selectedTab.header}</h3>
        {selectedTab.content}
      </div>
    </>
  );
}

На відміну від передачі дочірніх елементів як JSX, цей підхід дозволяє вам пов'язати деякі додаткові дані, такі як заголовок з кожним елементом. Оскільки ви працюєте з tabs безпосередньо, і це масив, вам не потрібні методи Children.


Виклик рендер-пропу для налаштування рендерингу

Замість того, щоб створювати JSX для кожного окремого елемента, ви також можете передати функцію, яка повертає JSX, і викликати цю функцію за необхідності. У цьому прикладі компонент App передає функцію renderContent компоненту TabSwitcher. Компонент TabSwitcher викликає renderContent лише для вибраної вкладки:

import TabSwitcher from './TabSwitcher.js';

export default function App() {
  return (
    <TabSwitcher
      tabIds={['first', 'second', 'third']}
      getHeader={tabId => {
        return tabId[0].toUpperCase() + tabId.slice(1);
      }}
      renderContent={tabId => {
        return <p>This is the {tabId} item.</p>;
      }}
    />
  );
}
import { useState } from 'react';

export default function TabSwitcher({ tabIds, getHeader, renderContent }) {
  const [selectedId, setSelectedId] = useState(tabIds[0]);
  return (
    <>
      {tabIds.map((tabId) => (
        <button
          key={tabId}
          onClick={() => setSelectedId(tabId)}
        >
          {getHeader(tabId)}
        </button>
      ))}
      <hr />
      <div key={selectedId}>
        <h3>{getHeader(selectedId)}</h3>
        {renderContent(selectedId)}
      </div>
    </>
  );
}

Проп на кшталт renderContent називається пропом рендерингу, оскільки він вказує, як рендерити частину інтерфейсу користувача. Втім, у ньому немає нічого особливого: це звичайний проп, який є функцією.

Рендерні пропси є функціями, тому ви можете передавати їм інформацію. Наприклад, цей компонент RowList передає id та index кожного рядка до пропсу renderRow рендерингу, який використовує index для виділення парних рядків:

import { RowList, Row } from './RowList.js';

export default function App() {
  return (
    <RowList
      rowIds={['first', 'second', 'third']}
      renderRow={(id, index) => {
        return (
          <Row isHighlighted={index % 2 === 0}>
            <p>This is the {id} item.</p>
          </Row> 
        );
      }}
    />
  );
}
import { Fragment } from 'react';

export function RowList({ rowIds, renderRow }) {
  return (
    <div className="RowList">
      <h1 className="RowListHeader">
        Total rows: {rowIds.length}
      </h1>
      {rowIds.map((rowId, index) =>
        <Fragment key={rowId}>
          {renderRow(rowId, index)}
        </Fragment>
      )}
    </div>
  );
}

export function Row({ children, isHighlighted }) {
  return (
    <div className={[
      'Row',
      isHighlighted ? 'RowHighlighted' : ''
    ].join(' ')}>
      {children}
    </div>
  );
}
.RowList {
  display: flex;
  flex-direction: column;
  border: 2px solid grey;
  padding: 5px;
}

.RowListHeader {
  padding-top: 5px;
  font-size: 25px;
  font-weight: bold;
  text-align: center;
}

.Row {
  border: 2px dashed black;
  padding: 5px;
  margin: 5px;
}

.RowHighlighted {
  background: #ffa;
}

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


Налагодження

Я передаю користувацький компонент, але методи Children не показують його результат рендерингу

Припустимо, ви передаєте два дочірні елементи до RowList таким чином:

<RowList>
  <p>First item</p>
  <MoreRows />
</RowList>

Якщо ви зробите Children.count(children) всередині RowList, ви отримаєте 2. Навіть якщо MoreRows відрендерить 10 різних елементів або поверне null, Children.count(children) все одно буде 2. З точки зору RowList, він "бачить" лише отриманий JSX. Він не "бачить" нутрощі компонента MoreRows.

Обмеження ускладнює вилучення компонентів. Ось чому альтернативам надається перевага перед використанням Children.