В React children это пропс, в котором лежит всё, что вы пишете между открывающим и закрывающим тегами компонента. Компонент, который рендерит {children} где-то в своём выводе, становится обёрткой: он даёт рамку, а вызывающий код даёт содержимое.
Обе карточки используют одну рамку, один отступ и один стиль заголовка, но содержат совершенно разное. Добавьте третью <Card title="Help"> со ссылкой внутри, и она бесплатно получит ту же рамку.
Как children попадает в компонент
JSX превращает вложенное содержимое в пропс. Эти две строки создают одинаковый элемент:
<Card title="Profile"><p>Ada</p></Card>
<Card title="Profile" children={<p>Ada</p>} />
Значит, children это обычный пропс с особым способом передачи. Его деструктурируют как любой другой (function Card({ children })) или читают как props.children. Пропсы в целом разобраны на странице о пропсах.
Что придёт в children, зависит от того, что вы написали между тегами. Один дочерний элемент приходит как этот элемент. Несколько приходят массивом. Текст приходит строкой, а отсутствие содержимого приходит как undefined.
Консоль показывает object, array of 2, string и undefined. Обычно об этом можно не думать: рендер {children} справляется со всеми вариантами, а null, undefined, true и false ничего не рендерят. Форма важна, только если вы пытаетесь изучать или менять children, и именно от этого предостерегает последний раздел.
Компоненты макета
Самое частое применение children это компонент макета: каркас страницы, макет с боковой панелью, центрированная колонка. Компонент владеет структурой и отступами, а каждая страница, которая его использует, передаёт своё содержимое.
function PageLayout({ children }) {
return (
<div className="page">
<Header />
<main className="page-content">{children}</main>
<Footer />
</div>
);
}
function AboutPage() {
return (
<PageLayout>
<h1>About us</h1>
<p>We teach people to code.</p>
</PageLayout>
);
}
PageLayout не знает и не интересуется, что содержит страница «о нас». В этом разделении и смысл: измените шапку один раз, и её подхватит каждая страница, обёрнутая в макет.
Несколько слотов через именованные пропсы
children это один слот. Когда компоненту нужно содержимое в нескольких местах, передайте дополнительные части именованными пропсами. Любой пропс может хранить JSX, поэтому модальное окно может принимать title, footer и тело как children.
Оболочка модального окна управляет рамками, отступами и порядком трёх областей, а вызывающий код заполняет каждую. В других фреймворках это называют слотами; в React это просто пропсы. <>...</> вокруг двух кнопок это фрагмент, который группирует их без лишнего элемента. Настоящий диалог также рендерился бы в document.body через портал, чтобы быть поверх страницы.
Перенесите <p> в пропс title, и текст тела появится в шапке: компонент решает, куда идёт каждый пропс, а вызывающий код решает только, что в него положить.
Children как функция
Иногда обёртка владеет каким-то состоянием, а решать, как его нарисовать, должен вызывающий код. Вместо элемента вызывающий код передаёт в children функцию, а обёртка вызывает её со значениями. Этот шаблон называется render prop.
Один Toggle рисует кнопку, другой флажок, по одной и той же логике. До хуков render props были основным способом делиться логикой с состоянием. Сегодня пользовательский хук вроде useToggle() обычно читается лучше, но render props всё ещё встречаются в библиотеках для списков, форм и анимации.
API Children и cloneElement
React экспортирует объект Children с помощниками для обхода children: Children.map, Children.forEach, Children.count, Children.toArray и Children.only. Вместе с cloneElement, который копирует элемент с новыми пропсами, они позволяют родителю изучать и менять то, что ему передали. Команда React относит их к устаревшим API: они работают, но в новом коде их стоит избегать.
Причина в том, что они видят только элементы, написанные прямо между тегами. Они не видят, что эти элементы рендерят.
Список показывает четыре пункта, но консоль пишет Children.count = 3, потому что TwoMore это один дочерний элемент, сколько бы пунктов он ни рендерил. cloneElement также передаёт пропс style в TwoMore, который его игнорирует, поэтому третий и четвёртый пункты не полосатые. Любой, кто вынесет несколько <li> в компонент, сломает родителя, даже не трогая его.
Решение: перестать лезть в children и передавать данные:
function NumberedList({ items }) {
return (
<ol>
{items.map((item, i) => (
<li key={item.id} style={{ color: i % 2 ? 'gray' : 'black' }}>
{item.label}
</li>
))}
</ol>
);
}
Теперь список сам отвечает за рендер каждой строки, и ничто не зависит от того, как вызывающий код написал свой JSX. Когда родителю нужно поделиться значениями с глубоко вложенными детьми (выбранной вкладкой, темой), используйте контекст, а не клонирование пропсов на них.
Типизация children в TypeScript
В TypeScript задайте children тип React.ReactNode. Он покрывает всё, что React умеет рендерить: элементы, строки, числа, массивы из них, null, undefined и булевы значения.
import type { ReactNode } from 'react';
type CardProps = {
title: string;
children: ReactNode;
};
function Card({ title, children }: CardProps) {
return (
<section>
<h3>{title}</h3>
{children}
</section>
);
}
Для render prop укажите тип функции, которой он является: children: (on: boolean, toggle: () => void) => ReactNode. Сделайте children необязательным (children?: ReactNode), когда компонент имеет смысл и пустым.
Частые ошибки
Забыть отрендерить children. Если компонент принимает children, но не помещает {children} в свой вывод, содержимое молча исчезает. Никакого предупреждения не будет.
Вызывать children как функцию, когда это не функция. children() работает, только если вызывающий код передал функцию. Если компонент ожидает render prop, скажите об этом в его имени или документации или принимайте именованный пропс, например render, чтобы намерение было ясным.
Мутировать children. Элементы доступны только для чтения. В разработке React их замораживает, поэтому присваивание в children.props там выбрасывает ошибку. Вместо этого создавайте новый вывод.
Часто задаваемые вопросы
Что такое props.children в React?
Это содержимое, записанное между открывающим и закрывающим тегами компонента. В <Card><p>Hi</p></Card> функция Card получает <p> как props.children и решает, где его отрендерить.
children это особый пропс?
Только по способу передачи. JSX кладёт вложенное содержимое в пропс с именем children, но внутри компонента это обычный пропс. Его можно передать и явно, как в <Card children={<p>Hi</p>} />, и это работает так же.
Как передать компоненту больше одного блока содержимого?
Используйте именованные пропсы для дополнительных блоков. Пропс может хранить JSX так же, как children, поэтому <Modal title={<h2>Delete?</h2>} footer={<button>OK</button>}>Body</Modal> даёт компоненту три слота.
Стоит ли использовать Children.map и cloneElement?
В новом коде лучше их избегать. Они видят только элементы, написанные прямо между тегами, а не то, что эти элементы рендерят, поэтому ломаются, как только кто-то обернёт дочерний элемент в другой компонент. Вместо этого передавайте массив данных пропсом или используйте контекст.
Какой тип у children в TypeScript?
Используйте React.ReactNode. Он покрывает всё, что React умеет рендерить: элементы, строки, числа, массивы, null, undefined и булевы значения.