Пошук уроків, статей та іншого контенту
Передавайте UI-структуру через композицію та розпізнавайте ситуації, у яких prop drilling ускладнює архітектуру.
Prop drilling — це передавання props через кілька проміжних компонентів, яким ці дані не потрібні для власної роботи.
Наприклад, компонент App знає дані користувача й передає їх у Page. Page передає далі в Layout, а Layout — у UserMenu:
function App() {
const user = { name: "Олена" };
return <Page user={user} />;
}
function Page({ user }) {
return <Layout user={user} />;
}
function Layout({ user }) {
return <Header user={user} />;
}
function Header({ user }) {
return <UserMenu user={user} />;
}
function UserMenu({ user }) {
return <strong>{user.name}</strong>;
}Page, Layout і Header фактично працюють як транзитні вузли. Вони приймають user лише для того, щоб передати його далі.
Саме по собі передавання props через один або два рівні не є проблемою. Воно стає незручним, коли:
між джерелом даних і споживачем багато рівнів;
проміжні компоненти передають велику кількість props;
структура компонентів часто змінюється;
компоненти починають знати про деталі, які їм не потрібні;
зміни API дочірнього компонента змушують редагувати багато батьківських компонентів.
Композиція — це побудова складного UI з менших компонентів шляхом передавання готової структури інтерфейсу як props.
Замість того щоб змушувати Layout знати про користувача, меню та його дані, можна передати йому готовий елемент:
function App() {
const user = { name: "Олена" };
return (
<Layout
header={<UserMenu user={user} />}
content={<Dashboard />}
/>
);
}Тепер Layout відповідає лише за розташування частин сторінки:
function Layout({ header, content }) {
return (
<div className="layout">
<header>{header}</header>
<main>{content}</main>
</div>
);
}Layout не знає:
звідки взявся користувач;
як працює UserMenu;
які дані потрібні Dashboard;
як побудовані передані компоненти.
Це називають інверсією керування: компонент-контейнер визначає місце для UI, а батьківський компонент вирішує, що саме там показати.
children як основний інструмент композиціїСпеціальний prop children містить елементи, розташовані між відкривальним і закривальним тегами компонента.
function Card({ children }) {
return (
<section className="card">
{children}
</section>
);
}
function App() {
return (
<Card>
<h2>Налаштування профілю</h2>
<p>Змініть дані свого облікового запису.</p>
</Card>
);
}Card відповідає за спільну оболонку, але не визначає її вміст. Один і той самий компонент можна використовувати для різних сценаріїв:
function App() {
return (
<>
<Card>
<h2>Профіль</h2>
<p>Інформація про користувача.</p>
</Card>
<Card>
<h2>Сповіщення</h2>
<button type="button">Увімкнути</button>
</Card>
</>
);
}Це простіше підтримувати, ніж компонент із великою кількістю умовних props:
// Такий API швидко стає складним
<Card
title="Профіль"
description="Інформація про користувача"
showButton={false}
buttonText=""
showNotifications={false}
/>children достатньо, коли компонент має одну основну область для вмісту. Якщо UI має кілька незалежних областей, використовуйте props із готовими React-елементами.
function PageLayout({ sidebar, header, children }) {
return (
<div className="page-layout">
<aside>{sidebar}</aside>
<div className="page-content">
<header>{header}</header>
<main>{children}</main>
</div>
</div>
);
}
function App() {
return (
<PageLayout
header={<Header />}
sidebar={<Navigation />}
>
<Dashboard />
</PageLayout>
);
}Такий компонент описує лише каркас:
sidebar — бічна панель;
header — верхня частина;
children — основний вміст.
Він не прив’язаний до конкретної навігації чи конкретної сторінки.
Нижче наведено приклад сторінки профілю. App володіє даними користувача й передає готові частини UI до DashboardLayout. Проміжний компонент не передає ці дані через себе.
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./styles.css";
function App() {
const user = {
name: "Олена Коваль",
role: "Frontend Developer",
};
return (
<DashboardLayout
header={<UserMenu user={user} />}
sidebar={<Sidebar />}
>
<ProfileCard user={user} />
</DashboardLayout>
);
}
function DashboardLayout({ header, sidebar, children }) {
return (
<div className="dashboard-layout">
<aside className="sidebar">{sidebar}</aside>
<div className="workspace">
<header className="header">{header}</header>
<main className="main-content">{children}</main>
</div>
</div>
);
}
function UserMenu({ user }) {
return (
<div className="user-menu">
<span>{user.name}</span>
<button type="button">Вийти</button>
</div>
);
}
function Sidebar() {
return (
<nav>
<a href="#profile">Профіль</a>
<a href="#settings">Налаштування</a>
</nav>
);
}
function ProfileCard({ user }) {
return (
<section className="profile-card">
<h1>{user.name}</h1>
<p>{user.role}</p>
</section>
);
}
createRoot(document.getElementById("root")).render(
<StrictMode>
<App />
</StrictMode>
);У цьому прикладі:
App знає дані користувача;
UserMenu і ProfileCard отримують лише ті дані, які їм потрібні;
DashboardLayout не отримує user;
DashboardLayout не залежить від конкретного вмісту меню чи профілю;
заміна Sidebar або ProfileCard не потребує змін у структурі DashboardLayout.
Компонент із композицією зазвичай має чіткішу відповідальність:
function Modal({ title, children, actions }) {
return (
<div className="modal">
<h2>{title}</h2>
<div className="modal-body">{children}</div>
<footer>{actions}</footer>
</div>
);
}Батьківський компонент вирішує, який вміст передати:
function DeleteAccountDialog() {
return (
<Modal
title="Видалити обліковий запис?"
actions={
<>
<button type="button">Скасувати</button>
<button type="button">Видалити</button>
</>
}
>
<p>Цю дію неможливо скасувати.</p>
</Modal>
);
}Modal не містить логіки видалення облікового запису. Він лише надає структуру модального вікна.
Це корисно для компонентів, які відповідають за:
розкладку;
рамку або оболонку;
відступи;
заголовок і підвал;
стандартне розташування дочірнього UI.
Не кожне передавання props потрібно замінювати композицією.
Prop drilling може бути нормальним, якщо:
prop проходить лише через один-два компоненти;
проміжний компонент використовує його сам;
залежність легко зрозуміти з коду;
структура компонентів стабільна;
передається проста конфігурація, наприклад variant, size або disabled.
function SettingsPage() {
return <SettingsSection variant="compact" />;
}
function SettingsSection({ variant }) {
return <SettingsField variant={variant} />;
}
function SettingsField({ variant }) {
return <input className={variant} />;
}У такому випадку додавання іншого механізму може ускладнити код більше, ніж саме передавання props.
Важливо оцінювати не кількість рівнів як таку, а якість залежностей між компонентами.
Скористайтеся такими питаннями:
Чи потрібен prop проміжному компоненту для його власного рендерингу?
Якщо так, передавання props може бути доречним.
Якщо ні, розгляньте композицію.
Чи передає компонент готову частину UI далі без змін?
Якщо так, це ознака prop drilling.
Чи є компонент контейнером або layout-компонентом?
Якщо так, йому часто краще передавати children або іменовані області.
Чи має батьківський компонент знати конкретну реалізацію дочірнього UI?
Якщо ні, передавання готового елемента зменшить зв’язаність.
Чи не перетворюється API компонента на набір спеціальних прапорців?
Якщо так, композиція може зробити API простішим.
Якщо компонент використовує лише ім’я, не обов’язково передавати весь об’єкт користувача:
function UserName({ name }) {
return <span>{name}</span>;
}
function App() {
const user = {
name: "Олена",
email: "olena@example.com",
role: "admin",
};
return <UserName name={user.name} />;
}Це робить контракт компонента зрозумілішим. Водночас не потрібно механічно розкладати кожен об’єкт на десятки props — межа має бути практичною.
Композиція не означає, що кожен елемент потрібно передавати окремим prop:
<Page
header={header}
leftSidebar={leftSidebar}
rightSidebar={rightSidebar}
topActions={topActions}
breadcrumbs={breadcrumbs}
footer={footer}
/>Якщо API стає важким для читання, частину структури можна передати через children або розділити великий компонент на менші компоненти.
Це різні значення:
// React-елемент: уже створений UI
const header = <Header />;
// Посилання на компонент
const HeaderComponent = Header;Якщо компонент очікує готовий UI, передавайте елемент:
<Layout header={<Header />} />Якщо він має сам створити компонент із додатковими props, API буде іншим:
<Layout Header={Header} />Для звичайної композиції layout-компонентів частіше зручніший перший варіант.
Композиція зменшує prop drilling, але не замінює продуману структуру даних. Якщо батьківський компонент передає один і той самий великий об’єкт у багато незалежних частин UI, варто перевірити, чи справді ці частини мають спільну відповідальність.
Такий компонент поступово стає спеціалізованим:
function Layout({ showProfile, showSettings, showNotifications }) {
return (
<main>
{showProfile && <Profile />}
{showSettings && <Settings />}
{showNotifications && <Notifications />}
</main>
);
}Часто краще передати потрібний вміст ззовні:
function Layout({ children }) {
return <main>{children}</main>;
}
function App() {
return (
<Layout>
<Profile />
<Settings />
</Layout>
);
}Prop drilling — це передавання props через компоненти, яким ці props не потрібні.
Невелике prop drilling може бути простим і зрозумілим рішенням.
Композиція дає змогу передавати готові React-елементи через children або спеціальні props.
Layout-компонентам краще відповідати за структуру, а не за конкретний вміст.
Композиція зменшує зв’язаність між компонентами та кількість транзитних props.
Перед вибором рішення перевіряйте, чи використовує проміжний компонент передані дані.
Не слід замінювати композицією кожне передавання props: важливі зрозумілі межі відповідальності та простий API.