Добро пожаловать на 24-й урок нашего курса. Сегодня мы будем говорить не о новых хуках или сложных паттернах управления состоянием. Мы поговорим о чем-то более фундаментальном, о философии, которая делает React по-настоящему мощным. Мы изучим композицию компонентов, а именно как использовать props.children и передачу компонентов через пропсы для создания не просто компонентов, а гибких и переиспользуемых «кирпичиков» для нашего интерфейса.
Если до этого вы думали о компонентах как о замкнутых блоках, то сегодня мы научимся заставлять их общаться и комбинироваться таким образом, что ваши возможности по созданию интерфейсов вырастут в разы.
Использование props.children и передача компонентов через props для создания гибких «оберток»
Давайте начнем с простой аналогии. Представьте, что вы строите дом не из готовых комнат, а из кирпичей, балок и окон. Готовая комната, это как жестко закодированный компонент: у нее fixed размер, fixed расположение розеток, fixed цвет стен. Если вы захотите изменить что-то внутри, вам придется ломать стены или строить новую комнату.
А теперь представьте, что у вас есть каркас комнаты (компонент-«обертка») и вы можете решать, какие окна в него вставить (панорамные или маленькие), какую дверь поставить, где расположить мебель (все это дочерние компоненты или компоненты, передаваемые через пропсы). Это и есть композиция. Вы создаете гибкие структуры, которые определяют где и как что-то отображается, но не что именно. Содержимое определяется вами, разработчиком, при использовании этого компонента.
В React есть два основных способа реализовать этот подход: с помощью волшебного пропса children и через передачу JSX-элементов или целых компонентов в обычные пропсы.
Что такое props.children?
Пропс children это особый пропс в React, который автоматически передается каждому компоненту. Он содержит всё, что находится между открывающим и закрывающим тегами этого компонента в JSX.
Давайте сразу к примеру. Создадим простейший компонент-обертку Card.
// Card.jsx function Card(props) { return ( <div className="card" style={{ border: '1px solid #ccc', padding: '16px', borderRadius: '8px', margin: '10px' }}> {props.children} </div> ); } export default Card;
Теперь давайте его используем в нашем основном компоненте App.
// App.jsx import Card from './Card'; function App() { return ( <div> {/* Используем Card с разным содержимым */} <Card> <h2>Это простая карточка</h2> <p>А это ее текст, переданный как `children`.</p> </Card> <Card> <img src="/path/to/image.jpg" alt="Котик" style={{ width: '100%' }} /> <p>А вот карточка с изображением!</p> <button>Кнопка внутри карточки</button> </Card> </div> ); } export default App;
Что здесь произошло? В первом использовании Card между его тегами мы поместили заголовок <h2> и параграф <p>. React упаковал эти элементы в пропс props.children и передал их в функцию Card. Внутри Card мы говорим: «Вставь содержимое children вот в этот div с классами стилей».
Во втором случае children это уже картинка, параграф и кнопка. Компонент Card абсолютно не заботится о том, что это за содержимое. Его единственная задача, обеспечить ему «обертку», стилизованный контейнер.
В чем суть?
-
Переиспользуемость. Один компонент
Cardмы можем использовать для отображения абсолютно любого контента. -
Чистота и декларативность. Код в
App.jsxочень читаемый. Мы сразу видим структуру: вот карточка, а внутри нее заголовок и текст. -
Централизованное управление. Если мы захотим изменить стили всех карточек в приложении (например, поменять цвет границы или добавить тень), нам достаточно изменить код в одном месте, внутри компонента
Card.
children это ваш основной инструмент для создания компонентов-«контейнеров», «макетов» и «оберток».
Передача компонентов через обычные props
А что, если нам нужна еще большая гибкость? Представьте, что мы создаем компонент Layout, который имеет заголовок, сайдбар и основное содержимое. Но мы хотим, чтобы родительский компонент мог полностью контролировать, что именно рендерится в каждой из этих областей.
Здесь нам на помощь приходит передача JSX-элементов и компонентов через обычные пропсы. Вместо того чтобы использовать фиксированные children, мы можем определить несколько «слотов» для контента.
Давайте создадим компонент PageLayout.
// PageLayout.jsx function PageLayout({ header, sidebar, content }) { return ( <div className="page-layout"> <header className="layout-header"> {header} </header> <div className="layout-body"> <aside className="layout-sidebar"> {sidebar} </aside> <main className="layout-content"> {content} </main> </div> </div> ); } export default PageLayout;
Теперь используем его в App. Обратите внимание, мы передаем не строки, а целые JSX-выражения.
// App.jsx import PageLayout from './PageLayout'; function App() { // Допустим, у нас есть другие компоненты const UserProfile = () => <div>Профиль пользователя</div>; const NavigationMenu = () => <div>Меню навигации</div>; return ( <div> <PageLayout header={<h1>Мой Крутой Сайт</h1>} sidebar={<NavigationMenu />} content={ <div> <UserProfile /> <p>Добро пожаловать на главную страницу!</p> </div> } /> </div> ); } export default App;
Это открывает еще больше возможностей! Мы можем передавать в пропсы не только статичный JSX, но и результаты вызовов функций, условный рендеринг и, что самое главное, состояние и колбэки из родительского компонента. Давайте рассмотрим более продвинутый и классический пример, компонент Modal (модальное окно).
Практический пример: Создание универсального компонента Modal
Наша задача создать модальное окно, которое:
-
Имеет фон-оверлей.
-
Имеет само окно с возможностью задания заголовка.
-
Содержит основную область контента, куда можно поместить что угодно.
-
Имеет область для действий (кнопки), но при этом кнопки должны быть определены снаружи, в родительском компоненте, чтобы иметь доступ к его состоянию.
Создадим Modal.jsx:
// Modal.jsx function Modal({ isOpen, onClose, title, children, actions }) { // Если модальное окно закрыто, не рендерим ничего if (!isOpen) { return null; } return ( <div className="modal-overlay" style={{ /* стили для фона */ }}> <div className="modal-window" style={{ /* стили для окна */ }}> {/* Заголовок модалки */} <div className="modal-header"> <h2>{title}</h2> <button onClick={onClose} className="close-button">X</button> </div> {/* Основное содержимое, переданное как children */} <div className="modal-content"> {children} </div> {/* Область для кнопок, переданная через проп actions */} <div className="modal-actions"> {actions} </div> </div> </div> ); } export default Modal;
А теперь волшебство использования в App.jsx:
// App.jsx import { useState } from 'react'; import Modal from './Modal'; function App() { const [isModalOpen, setIsModalOpen] = useState(false); const [data, setData] = useState(''); const openModal = () => setIsModalOpen(true); const closeModal = () => setIsModalOpen(false); const handleSave = () => { alert(`Данные сохранены: ${data}`); closeModal(); }; return ( <div> <button onClick={openModal}>Открыть модальное окно</button> <Modal isOpen={isModalOpen} onClose={closeModal} title="Пример модального окна" // Содержимое модалки (children) children={ <div> <p>Это основная область контента. Здесь может быть что угодно!</p> <input type="text" value={data} onChange={(e) => setData(e.target.value)} placeholder="Введите что-нибудь" /> </div> } // Действия для модалки (actions) actions={ <> <button onClick={closeModal}>Отмена</button> <button onClick={handleSave}>Сохранить</button> </> } /> </div> ); } export default App;
Что мы здесь достигли?
-
Компонент
Modalабсолютно глуп. Он не знает, какой контент в него придет, какие кнопки будут и что они делают. -
Вся логика (состояние
data, функцииopenModal,closeModal,handleSave) живет в родительском компонентеApp. Это правильное расположение состояния («подъем состояния»). -
Кнопка «Сохранить» имеет доступ к состоянию
dataродительского компонента, потому что она объявлена прямо в нем. -
Мы получили супергибкий компонент
Modal, который можно использовать throughout the whole app с абсолютно разным содержимым и действиями.
Это и есть мощь композиции. Мы разделили ответственность: Modal отвечает за внешний вид и поведение окна (закрытие по кнопке, оверлей), а App за его наполнение и бизнес-логику.
Комбинирование подходов
Часто в реальных проектах эти подходы комбинируются. У вас может быть компонент, который ожидает основное содержимое через children, но при этом имеет несколько именованных «слотов» для специфического контента.
Допустим, компонент DataGrid:
// DataGrid.jsx function DataGrid({ data, children, toolbar, pagination }) { return ( <div className="data-grid"> {/* Слот для тулбара с кнопками */} {toolbar && <div className="data-grid-toolbar">{toolbar}</div>} {/* Основное содержимое таблицы, переданное как children */} <table className="data-grid-table"> {children} </table> {/* Слот для пагинации */} {pagination && <div className="data-grid-pagination">{pagination}</div>} </div> ); } export default DataGrid;
Использование:
<DataGrid data={someData} toolbar={<button>Экспорт</button>} pagination={<PaginationComponent />} > {/* Всё, что здесь это props.children и будет вставлено внутрь тега <table> */} <thead>...</thead> <tbody>...</tbody> </DataGrid>
Когда что использовать?
-
Используйте
props.children, когда ваш компонент является простой оберткой вокруг неизвестного контента.Card,Modal(для основного контента),Layout,Buttonидеальные кандидаты. -
Используйте передачу через именованные пропсы, когда вам нужно несколько отдельных «слотов» для контента в вашем компоненте.
PageLayoutсheader,sidebar,contentилиDataGridсtoolbarиpagination. -
Комбинируйте оба подхода, когда есть основное содержимое (
children) и дополнительные, опциональные блоки.
Чего следует избегать
Начинающие разработчики иногда пытаются передавать всё и вся через пропсы, включая сложные объекты и массивы данных, а затем внутри компонента-обертки рендерить их по сложной логике. Это усложняет компонент и делает его менее переиспользуемым.
Плохо:
// Антипаттерн: Компонент знает слишком много <UserList users={users} showEmail={true} onEditUser={handleEditUser} onDeleteUser={handleDeleteUser} canEdit={userPermissions.canEdit} // ... и еще 10 пропсов />
В этом случае UserList становится монолитом, который тяжело тестировать и переиспользовать.
Хорошо (используя композицию):
// Паттерн композиции: Компонент, это гибкая обертка <UserList> {users.map(user => ( <UserListItem key={user.id} user={user} // Действия передаются как пропсы в Item или даже как children для UserListItem actions={ <> <button onClick={() => handleEditUser(user.id)}>Edit</button> <button onClick={() => handleDeleteUser(user.id)}>Delete</button> </> } /> ))} </UserList>
Здесь UserList отвечает только за отображение списка (например, <ul>), а UserListItem за отображение каждого элемента. Логика действий инкапсулирована там, где она используется.
Практические задачи для закрепления
Чтобы материал точно отложился в голове, выполните следующие задачи. Не просто скопируйте код, а попробуйте написать его сами.
Задача 1: Умная кнопка с иконкой
Создайте компонент IconButton. Он должен принимать пропсы icon (компонент иконки, например, из React Icons), onClick и children (текст кнопки). Компонент должен рендерить кнопку, внутри которой слева отображается переданная иконка, а справа текст.
Пример использования:
<IconButton icon={<FiSave />} onClick={handleSave}> Сохранить черновик </IconButton>
Задача 2: Универсальная карточка с заголовком и действиями
Усовершенствуйте наш ранний компонент Card. Он должен принимать пропсы title (строка), actions (JSX с кнопками) и children (основной контент). Внутри компонента рендерите заголовок, основное содержимое и кнопки действий в нижней части карточки.
Пример использования:
<Card title="Профиль пользователя" actions={<button>Редактировать</button>} > <p>Имя: Иван Иванов</p> <p>Email: ivan@example.com</p> </Card>
Задача 3: Компонент Accordion (раскрывающийся список)
Создайте компонент Accordion, который состоит из:
-
AccordionItemэлемент аккордеона, принимающийtitle(заголовок) иchildren(скрытое содержимое). -
Логика должна быть такой. что только один пункт аккордеона может быть открыт одновременно. При клике на заголовок другого пункта, предыдущий закрывается.
Подсказка. Вам потребуется поднять состояние активного элемента на уровень выше, в компонент Accordion. Accordion будет хранить activeIndex и функцию для его изменения. Затем через пропсы передавать в AccordionItem информацию о том, открыт ли он и колбэк для переключения.
// Примерная структура использования <Accordion> <AccordionItem title="Раздел 1"> <p>Содержимое первого раздела.</p> </AccordionItem> <AccordionItem title="Раздел 2"> <p>Содержимое второго раздела.</p> </AccordionItem> </Accordion>
Эта задача объединит всё, что мы прошли: композицию, управление состоянием и передачу колбэков.
Заключение
Композиция, это образ мышления. Вместо того чтобы создавать монолитные, сложные компоненты, старайтесь думать о том, как разбить интерфейс на маленькие, управляемые, гибкие кусочки, которые можно комбинировать, как конструктор Лего.
Использование children и передача компонентов через пропсы, это ваш ключ к созданию по-настоящему переиспользуемого и поддерживаемого кода.
У вас обязательно всё получится. Если остались вопросы, не стесняйтесь пересматривать урок и экспериментировать в песочнице. Удачи и до встречи на следующем уроке.
Для тех, кто хочет пройти курс полностью и стать полноценным React-разработчиком, все уроки собраны в единый курс: Полный курс с уроками по React для начинающих.
Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.


