Пошук уроків, статей та іншого контенту
Побудуєте гнучкі компоненти через композицію, обгортки та кілька іменованих точок вставки контенту.
Композиція — це підхід, за якого складний компонент будується з менших компонентів і дозволяє батьківському коду керувати частинами його вмісту.
Замість компонента з великою кількістю умов:
<Card
title="Профіль"
showHeader
showFooter
footerText="Зберегти"
headerIcon="user"
contentType="profile"
/>можна створити універсальну оболонку й передати потрібний вміст через children та окремі props:
<Card
header={<CardTitle>Профіль</CardTitle>}
footer={<button>Зберегти</button>}
>
<ProfileForm />
</Card>Такий підхід має кілька переваг:
компоненти менше залежать від конкретного контенту;
структуру інтерфейсу видно безпосередньо в місці використання;
не потрібно додавати новий prop для кожного можливого варіанта;
компоненти можна повторно використовувати в різних сценаріях.
children як основна точка композиціїProp children містить усе, що передано між відкривальним і закривальним тегами компонента.
function Panel({ children }) {
return (
<section className="panel">
{children}
</section>
);
}
function App() {
return (
<Panel>
<h2>Налаштування</h2>
<p>Керуйте параметрами профілю.</p>
</Panel>
);
}У цьому прикладі Panel не знає, який саме контент він відображатиме. Він відповідає лише за спільну оболонку.
children може бути:
одним елементом;
кількома елементами;
текстом;
результатом умовного виразу;
масивом елементів;
null, якщо контент не передано.
<Panel>
<p>Один дочірній елемент</p>
</Panel>
<Panel>
<h2>Заголовок</h2>
<p>Опис</p>
</Panel>
<Panel>
{isLoading ? <Spinner /> : <Content />}
</Panel>Компонент-обгортка створює спільну структуру навколо довільного контенту.
Наприклад, Card може відповідати за рамку, відступи та тінь, але не за конкретний вміст картки:
function Card({ children }) {
return (
<article className="card">
{children}
</article>
);
}Тепер одна й та сама обгортка підходить для різних сценаріїв:
<Card>
<h2>Останні замовлення</h2>
<OrdersList />
</Card>
<Card>
<h2>Статистика</h2>
<Statistics />
</Card>Це краще, ніж створювати окремі компоненти на кшталт OrdersCard і StatisticsCard, якщо їхня спільна поведінка обмежується візуальною оболонкою.
Обгортка часто має приймати стандартні HTML-атрибути. Для цього можна використати залишковий параметр ...rest:
function Card({ children, className = "", ...rest }) {
return (
<article className={`card ${className}`} {...rest}>
{children}
</article>
);
}Використання:
<Card
className="featured-card"
aria-label="Рекомендований товар"
>
<h2>Товар дня</h2>
</Card>Тут:
className обробляється окремо;
інші атрибути, зокрема aria-label, передаються до article;
children вставляється всередину обгортки.
children зручний для одного основного місця вставки. Але складний компонент часто має кілька областей:
заголовок;
основний вміст;
додаткові дії;
нижній колонтитул.
У React для цього використовують окремі props, які містять JSX. Їх часто називають іменованими слотами.
function Card({ title, actions, children, footer }) {
return (
<article className="card">
<header className="card__header">
<h2>{title}</h2>
<div className="card__actions">
{actions}
</div>
</header>
<div className="card__content">
{children}
</div>
{footer && (
<footer className="card__footer">
{footer}
</footer>
)}
</article>
);
}Тепер кожна частина картки передається окремо:
<Card
title="Профіль користувача"
actions={<button type="button">Редагувати</button>}
footer={<small>Оновлено сьогодні</small>}
>
<p>Основна інформація про користувача.</p>
</Card>children є основним вмістом, а title, actions і footer — додатковими іменованими точками вставки.
footer &&Якщо footer не передано, його значення зазвичай дорівнює undefined. Умова:
{footer && <footer>{footer}</footer>}не створює порожній елемент footer, якщо контент відсутній.
Це корисно, коли секція має бути необов’язковою:
function Dialog({ title, children, actions }) {
return (
<section className="dialog">
<h2>{title}</h2>
<div className="dialog__content">{children}</div>
{actions && (
<div className="dialog__actions">
{actions}
</div>
)}
</section>
);
}Prop може містити не лише рядок або число, а й готовий React-елемент:
function PageSection({ heading, aside, children }) {
return (
<section className="page-section">
<div className="page-section__heading">
<h2>{heading}</h2>
{aside}
</div>
<div className="page-section__content">
{children}
</div>
</section>
);
}Використання:
<PageSection
heading="Коментарі"
aside={<button type="button">Додати коментар</button>}
>
<CommentsList />
</PageSection>PageSection не знає, що саме буде в aside. Це може бути кнопка, посилання, лічильник або кілька елементів:
<PageSection
heading="Файли"
aside={
<>
<span>12 файлів</span>
<button type="button">Завантажити</button>
</>
}
>
<FileList />
</PageSection>Нижче наведено приклад сторінки з універсальною карткою. Картка має:
заголовок;
необов’язкові дії;
основний вміст через children;
необов’язковий нижній колонтитул.
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./styles.css";
function Card({ title, actions, children, footer }) {
return (
<article className="card">
<header className="card__header">
<h2 className="card__title">{title}</h2>
{actions && (
<div className="card__actions">
{actions}
</div>
)}
</header>
<div className="card__content">
{children}
</div>
{footer && (
<footer className="card__footer">
{footer}
</footer>
)}
</article>
);
}
function ProfileInfo() {
return (
<dl>
<div>
<dt>Ім’я</dt>
<dd>Олена Коваль</dd>
</div>
<div>
<dt>Email</dt>
<dd>olena@example.com</dd>
</div>
</dl>
);
}
function Notifications() {
return (
<ul>
<li>Новий коментар до вашого завдання</li>
<li>Ваш профіль оновлено</li>
</ul>
);
}
function App() {
return (
<main className="page">
<Card
title="Профіль"
actions={
<button type="button">
Редагувати
</button>
}
footer={<span>Останнє оновлення: сьогодні</span>}
>
<ProfileInfo />
</Card>
<Card
title="Сповіщення"
actions={
<button type="button">
Позначити все як прочитане
</button>
}
>
<Notifications />
</Card>
</main>
);
}
createRoot(document.getElementById("root")).render(
<StrictMode>
<App />
</StrictMode>
);Мінімальні стилі для цього прикладу:
body {
margin: 0;
font-family: sans-serif;
background: #f4f5f7;
}
.page {
display: grid;
gap: 16px;
max-width: 720px;
margin: 40px auto;
padding: 0 16px;
}
.card {
overflow: hidden;
background: white;
border: 1px solid #d9dce1;
border-radius: 8px;
}
.card__header,
.card__footer {
padding: 16px;
background: #fafafa;
}
.card__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
border-bottom: 1px solid #d9dce1;
}
.card__title {
margin: 0;
font-size: 20px;
}
.card__content {
padding: 16px;
}
.card__footer {
border-top: 1px solid #d9dce1;
color: #666;
font-size: 14px;
}
dl,
ul {
margin: 0;
padding: 0;
}
dl > div {
display: flex;
gap: 16px;
padding: 8px 0;
}
dt {
width: 80px;
font-weight: bold;
}
dd {
margin: 0;
}
li {
margin-left: 20px;
}Зверніть увагу: Card не містить умов для ProfileInfo або Notifications. Він лише визначає структуру та місця, у які вставляється переданий контент.
Невдалий варіант — створювати універсальний компонент, який знає про всі можливі типи контенту:
function Panel({ type, data }) {
if (type === "profile") {
return <Profile data={data} />;
}
if (type === "notifications") {
return <Notifications data={data} />;
}
if (type === "orders") {
return <Orders data={data} />;
}
return null;
}Такий компонент поступово стає залежним від усієї предметної області. Кожен новий тип контенту вимагає змін у ньому.
Композиція розділяє відповідальність:
function Panel({ title, children }) {
return (
<section className="panel">
<h2>{title}</h2>
{children}
</section>
);
}
function Dashboard() {
return (
<>
<Panel title="Профіль">
<Profile />
</Panel>
<Panel title="Сповіщення">
<Notifications />
</Panel>
<Panel title="Замовлення">
<Orders />
</Panel>
</>
);
}Panel відповідає за оболонку, а конкретний дочірній компонент — за власний вміст.
childrenВикористовуйте children, коли компонент має одну основну область довільного вмісту:
<Modal>
<LoginForm />
</Modal>Використовуйте окремий prop, коли в компонента є конкретна іменована область:
<Modal
title="Вхід"
actions={<LoginActions />}
>
<LoginForm />
</Modal>Зазвичай добре працює така схема:
children — головний вміст;
title — просте значення, якщо потрібен лише текст;
header — JSX, якщо заголовок може мати складну структуру;
actions — кнопки або інші елементи керування;
footer — необов’язкова нижня секція.
Якщо проп має приймати готовий JSX, назва повинна описувати його роль:
function Toolbar({ leftContent, rightContent }) {
return (
<div className="toolbar">
<div>{leftContent}</div>
<div>{rightContent}</div>
</div>
);
}Такі назви зрозуміліші за загальні content1 і content2.
Компонент стає складним, якщо для кожного маленького фрагмента додаються окремі props:
function Card({
title,
titleColor,
titleSize,
showTitleIcon,
titleIcon,
bodyText,
bodyColor,
footerText,
showFooter
}) {
// Компонент поступово перетворюється на набір винятків
}Якщо частина інтерфейсу може змінюватися, краще передати її як JSX:
<Card
header={
<>
<span>Профіль</span>
<StatusBadge status="active" />
</>
}
>
<ProfileInfo />
</Card>Не потрібно передавати розмітку рядком:
<Card header="<strong>Профіль</strong>" />React відобразить це як звичайний текст. Передавайте React-елемент:
<Card header={<strong>Профіль</strong>} />childrenЯкщо компонент приймає контент між тегами, він має дістати children з props і відрендерити його:
function Box({ children }) {
return <div className="box">{children}</div>;
}Без {children} переданий вміст не з’явиться на сторінці.
Якщо слот необов’язковий, не завжди варто створювати його оболонку:
function Card({ footer, children }) {
return (
<article>
{children}
<footer>{footer}</footer>
</article>
);
}Коли footer не передано, у DOM залишиться порожній footer. Для необов’язкової секції використовуйте умовний рендеринг:
function Card({ footer, children }) {
return (
<article>
{children}
{footer && <footer>{footer}</footer>}
</article>
);
}Композиція не означає, що кожен текст потрібно перетворювати на окремий JSX prop. Якщо компоненту достатньо простого значення, використовуйте простий prop:
<Card title="Профіль">
<ProfileInfo />
</Card>Немає потреби робити так:
<Card title={<span>Профіль</span>}>
<ProfileInfo />
</Card>JSX у prop доречний, коли потрібна додаткова структура або поведінка.
Під час створення компонента з композицією:
Визначте спільну структуру, яку має контролювати компонент.
Передайте основний змінний вміст через children.
Для кількох окремих областей створіть іменовані props.
Зробіть необов’язкові області умовними.
Не додавайте props для всіх можливих варіантів внутрішнього вмісту.
Передавайте складні частини як JSX або дочірні компоненти.
Перевірте, чи можна використати обгортку в іншому сценарії без її зміни.
Композиція дозволяє будувати складні інтерфейси з незалежних компонентів.
children — основний спосіб передати довільний вміст усередину обгортки.
Компонент може мати кілька іменованих точок вставки через props, наприклад header, actions і footer.
JSX можна передавати як значення prop.
Обгортка має відповідати за структуру, а не знати всі типи контенту, які в ній відображаються.
Умовний рендеринг допомагає не створювати порожні необов’язкові секції.
Композиція зменшує кількість умов і спеціальних props у багаторазових компонентах.