Пошук уроків, статей та іншого контенту
Проєктуйте повторно використовувані компоненти з чітким API, мінімальною зв’язаністю та передбачуваними варіантами налаштування.
Повторно використовуваний компонент можна застосувати в різних частинах інтерфейсу без копіювання його внутрішньої реалізації.
Він має:
чіткий набір вхідних параметрів;
передбачувану поведінку;
мінімальну залежність від конкретної сторінки або бізнес-логіки;
можливість налаштування без зміни внутрішнього коду;
зрозуміле співвідношення між відповідальністю компонента та його API.
Наприклад, компонент Panel має відповідати за відображення панелі: заголовка, додаткових дій і вмісту. Він не повинен самостійно завантажувати користувача, змінювати маршрут або знати, на якій сторінці використовується.
<Panel
title="Налаштування профілю"
actions={<Button>Зберегти</Button>}
>
<ProfileForm />
</Panel>У цьому прикладі:
Panel відповідає за структуру та оформлення;
Button відповідає за кнопку;
ProfileForm відповідає за форму;
сторінка з’єднує ці частини разом.
Таке розділення зменшує зв’язаність і спрощує повторне використання.
Перед створенням компонента сформулюйте його відповідальність одним реченням.
Panelвідображає контейнер із заголовком, діями та вмістом.
Якщо опис виходить надто довгим, компонент, імовірно, виконує забагато роботи.
Невдалий приклад:
<UserSettingsPanel
userId={userId}
loadUser={loadUser}
saveUser={saveUser}
redirectAfterSave
showNotifications
permissions={permissions}
/>Такий компонент одночасно:
завантажує дані;
відображає інтерфейс;
зберігає дані;
показує сповіщення;
виконує навігацію;
перевіряє дозволи.
Його складно використовувати поза одним конкретним сценарієм.
Краще розділити відповідальності:
<Panel
title="Налаштування"
actions={<SaveButton onClick={handleSave} />}
>
<SettingsForm
value={settings}
onChange={setSettings}
/>
</Panel>Компонент контейнера відповідає за представлення, а логіка сторінки залишається на рівні, де вона потрібна.
Props — це публічний API компонента. Кожен prop має бути виправданим і мати зрозумілу роль.
Зазвичай API компонента складається з таких категорій:
основний вміст через children;
значення, які впливають на вигляд;
обробники подій;
додаткові області для композиції;
рідше — передавання атрибутів до кореневого елемента.
Наприклад:
<Panel
title="Останні замовлення"
variant="muted"
actions={<Button size="small">Переглянути всі</Button>}
>
<OrdersList />
</Panel>Тут:
title задає заголовок;
variant задає один із дозволених варіантів оформлення;
actions є областю для довільного вмісту;
children містить основний вміст панелі.
children для основного вмістуЯкщо компонент має обгортати довільний React-вміст, зазвичай краще використовувати children, а не prop із вузькою назвою.
Менш гнучкий API:
<Panel content={<ProfileForm />} />Гнучкіший API:
<Panel>
<ProfileForm />
</Panel>children дає змогу передавати один елемент, кілька елементів, текст або умовний вміст без зміни API компонента.
Якщо компонент має кілька логічних областей, для них можна використати окремі props:
<Panel
title="Команда"
actions={<TeamActions />}
footer={<Pagination />}
/>Це зрозуміліше, ніж передавати масив елементів і покладатися на їхній порядок:
<Panel
slots={[
<TeamActions />,
<TeamList />,
<Pagination />
]}
/>Іменовані props документують структуру компонента без додаткових правил.
Композиція означає, що компонент отримує готові частини інтерфейсу ззовні, замість того щоб створювати їх самостійно.
Розглянемо реалізацію повторно використовуваної панелі:
import { createRoot } from "react-dom/client";
const panelStyles = {
base: {
borderRadius: 12,
border: "1px solid #d7dce3",
padding: 20,
backgroundColor: "#ffffff",
},
muted: {
backgroundColor: "#f5f7fa",
},
highlighted: {
borderColor: "#2563eb",
boxShadow: "0 4px 14px rgba(37, 99, 235, 0.12)",
},
};
function Button({ children, variant = "primary", onClick, size = "medium" }) {
const colors = {
primary: {
backgroundColor: "#2563eb",
color: "#ffffff",
},
secondary: {
backgroundColor: "#e5e7eb",
color: "#111827",
},
};
const sizes = {
small: {
padding: "6px 10px",
fontSize: 14,
},
medium: {
padding: "9px 14px",
fontSize: 16,
},
};
return (
<button
type="button"
onClick={onClick}
style={{
border: 0,
borderRadius: 6,
cursor: "pointer",
...colors[variant],
...sizes[size],
}}
>
{children}
</button>
);
}
function Panel({
title,
actions,
children,
variant = "base",
footer,
}) {
const selectedStyle = panelStyles[variant] ?? panelStyles.base;
return (
<section style={{ ...panelStyles.base, ...selectedStyle }}>
<header
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 16,
marginBottom: 16,
}}
>
<h2 style={{ margin: 0, fontSize: 20 }}>{title}</h2>
{actions ? (
<div style={{ display: "flex", gap: 8 }}>{actions}</div>
) : null}
</header>
<div>{children}</div>
{footer ? (
<footer style={{ marginTop: 16 }}>{footer}</footer>
) : null}
</section>
);
}
function App() {
function handleSave() {
window.alert("Налаштування збережено");
}
return (
<main
style={{
maxWidth: 640,
margin: "40px auto",
padding: "0 16px",
fontFamily: "sans-serif",
}}
>
<Panel
title="Профіль"
variant="highlighted"
actions={
<Button size="small" onClick={handleSave}>
Зберегти
</Button>
}
footer={<small>Остання зміна: сьогодні</small>}
>
<p>Керуйте основною інформацією свого профілю.</p>
<label>
Ім’я
<input
defaultValue="Олена"
style={{
display: "block",
width: "100%",
boxSizing: "border-box",
marginTop: 6,
padding: 8,
}}
/>
</label>
</Panel>
</main>
);
}
createRoot(document.getElementById("root")).render(<App />);Panel не знає:
що саме розміщено всередині;
що робить кнопка;
звідки беруться дані;
куди потрібно переходити після натискання;
чи є форма контрольованою.
Він лише відображає передану структуру. Це і є мінімальна зв’язаність.
Для налаштування вигляду краще використовувати обмежену кількість явних варіантів:
<Panel variant="muted">...</Panel>
<Panel variant="highlighted">...</Panel>Значення variant утворюють частину контракту компонента. Їх має бути небагато, і кожне повинно мати зрозуміле призначення.
Небажано створювати багато незалежних boolean-props:
<Panel
isBlue
hasShadow
isCompact
hasBorder
isHighlighted
/>Такі props швидко створюють неочевидні комбінації. Наприклад, незрозуміло, що станеться, якщо одночасно передати isBlue і isHighlighted.
Краще об’єднати пов’язані налаштування:
<Panel variant="highlighted" padding="compact">
...
</Panel>Або зафіксувати готові варіанти, якщо комбінації не повинні бути довільними:
<Panel variant="compact-highlighted">
...
</Panel>Вибір залежить від того, чи справді ці властивості мають комбінуватися.
Значення за замовчуванням спрощують використання компонента:
function Panel({
children,
variant = "base",
footer,
}) {
// ...
}Тепер базовий варіант не потрібно вказувати щоразу:
<Panel>Вміст</Panel>Значення за замовчуванням мають бути:
найпоширенішими;
безпечними;
сумісними з очікуваною поведінкою;
достатньо простими для розуміння.
Якщо prop є необов’язковим, компонент повинен коректно працювати без нього. У прикладі Panel не створює порожній блок для actions або footer, якщо ці props не передані.
Повторно використовуваний компонент зазвичай не повинен отримувати весь об’єкт із внутрішньою структурою бізнес-моделі, якщо для відображення потрібні лише окремі значення.
Менш прозорий API:
<UserCard user={user} />Тут компонент може неочікувано залежати від великої кількості полів об’єкта user.
Чіткіший API:
<UserCard
name={user.displayName}
email={user.email}
avatarUrl={user.avatarUrl}
/>Такий підхід:
робить залежності видимими;
спрощує тестування;
дозволяє використовувати компонент із даними іншого джерела;
зменшує вплив змін у структурі бізнес-моделі.
Це не означає, що передавання об’єктів завжди неправильне. Воно виправдане, якщо компонент справді працює з цілісною структурою даних і цей контракт є стабільним.
Компонент має отримувати лише ті обробники, які потрібні для його роботи.
Наприклад, кнопці достатньо знати про дію натискання:
<Button onClick={handleSave}>Зберегти</Button>Їй не потрібно отримувати весь об’єкт сторінки:
<Button
page={page}
user={user}
settings={settings}
onSave={handleSave}
/>Також не варто змушувати презентаційний компонент самостійно викликати API:
function Panel({ userId }) {
// Компонент стає залежним від конкретного способу отримання даних
// і більше не є універсальним.
}Краще завантажити дані на рівні сторінки або спеціалізованого компонента, а в повторно використовуваний компонент передати вже готовий вміст.
Велика кількість props часто означає одну з трьох проблем:
компонент виконує забагато відповідальностей;
частину властивостей можна замінити композицією;
кілька props описують один концепт і мають бути об’єднані у варіант або окремий компонент.
Наприклад, такий API важко підтримувати:
<Modal
title="Видалити файл"
showCloseButton
closeOnBackdropClick
closeOnEscape
showCancelButton
showConfirmButton
confirmButtonText="Видалити"
confirmButtonColor="danger"
isLoading={isDeleting}
onConfirm={handleDelete}
onCancel={handleCancel}
/>Частину структури можна передати через композицію:
<Modal
title="Видалити файл"
footer={
<>
<Button variant="secondary" onClick={handleCancel}>
Скасувати
</Button>
<Button onClick={handleDelete}>
Видалити
</Button>
</>
}
>
Ви впевнені, що хочете видалити файл?
</Modal>Тоді Modal відповідає за контейнер і поведінку модального вікна, а сторінка вирішує, які саме дії відобразити.
Перед фіналізацією компонента перевірте його в кількох різних контекстах:
без необов’язкових props;
з довгим заголовком;
без дій;
з кількома діями;
з різним типом children;
у базовому та спеціальному варіанті.
Якщо для кожного нового сценарію доводиться додавати спеціальний prop, API може бути спроєктований занадто вузько. Спробуйте використати композицію або передати потрібний вміст через children чи іменований slot-prop.
function Panel() {
const orders = getOrdersForDashboard();
// Панель більше не можна легко використати в іншому місці.
}Компонент оформлення не повинен містити логіку, специфічну для Dashboard.
<Card
isLarge
isBlue
hasBorder
hasShadow
isInteractive
isCompact
/>Такі props ускладнюють передбачення результату. Використовуйте обмежені варіанти або композицію.
<Panel
user={user}
organization={organization}
permissions={permissions}
settings={settings}
/>Якщо панелі потрібен лише заголовок і вміст, передавайте саме їх.
<Panel sections={[header, body, footer]} />Порядок елементів доводиться запам’ятовувати, а API не пояснює, що означає кожна позиція. Для іменованих областей використовуйте окремі props.
Не додавайте prop лише тому, що він може знадобитися колись у майбутньому. Спочатку перевірте реальний сценарій і знайдіть найпростішу модель, яка його покриває.
Повторно використовуваний компонент має одну чітку відповідальність.
Props є публічним API, тому вони повинні бути мінімальними й зрозумілими.
children підходить для основного довільного вмісту.
Іменовані props зручні для окремих областей, як-от actions або footer.
Композиція зменшує зв’язаність і робить компоненти гнучкішими.
Для стилів краще мати кілька явних варіантів, ніж багато boolean-props.
Бізнес-логіку, завантаження даних і навігацію слід залишати поза презентаційним компонентом.
Хороший API дозволяє змінювати внутрішню реалізацію компонента без змін у коді, який його використовує.