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, отримане вашим компонентом.- опція
thisArg
:це
значення, з яким слід викликати функціюfn
. Якщо його опустити, це будеundefined
.
forEach
методу зворотного виклику. Він буде викликаний з дочірнім елементом як першим аргументом і його індексом як другим аргументом. Індекс починається з 0
і збільшується при кожному виклику.
Повертає
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
, містить лише два елементи замість трьох:
<p>This is the first item.</p>
<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
.