Композиція проти наслідування в React
Composition vs inheritance в React - composition (композиція) збирає UI з вкладених компонентів через props типу children, а inheritance (наслідування) розширює ієрархії класів через extends.
Теорія
TL;DR
- Inheritance - як матрьошка: одна фігурка всередині іншої у фіксованому порядку. Composition - як LEGO: з'єднуєш будь-які блоки.
- Головна різниця: composition дає змогу повторно використовувати компоненти без жорстких ланцюжків класів.
- React docs прямо кажуть: не створюй власні ієрархії наслідування, крім
React.Component. - Правило вибору: у 99% випадків використовуй composition. Решту 1% де раніше потрібна була спільна логіка закривають hooks.
Швидкий приклад
// Inheritance (уникати): жорстко і крихко
class Popup extends React.Component {
render() { return <div className="popup">{this.props.children}</div>; }
}
class Dialog extends Popup { // прив'язаний до структури Popup
// будь-яка зміна в Popup ламає Dialog
}
// Composition (краще): гнучко
function Popup({ children }: { children: React.ReactNode }) {
return <div className="popup">{children}</div>;
}
function Dialog({ title, children }: { title: string; children: React.ReactNode }) {
return (
<Popup>
<h2>{title}</h2>
{children}
</Popup>
);
}Dialog обгортає Popup, не наслідуючи його. Можеш замінити Popup на будь-що без змін у Dialog.
Ключова різниця
Inheritance класів створює один ланцюжок прототипів. Зміни в базовому класі ламають усіх нащадків. Composition передає дані через props і тримає компоненти незалежними. Архітектура fiber в React обходить JSX-дерево зверху вниз, монтуючи кожен дочірній компонент окремо зі своїм станом. Inheritance плющить це в один об'єкт. Тому composition добре поєднується з hooks, а команда React перестала рекомендувати ієрархії класів після появи hooks у версії 16.8.
Коли що використовувати
- Загальний контейнер для довільного контенту (модалка, картка, лейаут) - prop
children. - Кілька поведінок на одному компоненті (стан завантаження плюс tooltip на кнопці) - composition через HOC або hooks.
- Спільний стан по всьому застосунку (авторизація, тема, локаль) - Context API, не inheritance.
- Просте візуальне перевикористання (картка, бейдж, лейаут) - composition завжди.
Таблиця порівняння
| Аспект | Composition | Inheritance |
|---|---|---|
| Гнучкість | Довільна комбінація компонентів | Фіксований ланцюжок класів |
| Перевикористання | Обгортання будь-яких UI-елементів | Тільки прямі нащадки |
| Підтримка коду | Незалежна зміна компонентів | Зміна базового класу ламає все |
| Підтримка React | Офіційний патерн, hooks-friendly | Не рекомендовано після React.Component |
| Коли використовувати | 99% випадків | Тільки legacy-код з класами |
Типові помилки
Помилка: власні ієрархії класів
// Неправильно: крихкий ланцюжок
class ThemedButton extends React.Component { /* prop теми */ }
class PrimaryButton extends ThemedButton { /* ламається при зміні теми */ }
// Виправлення: prop variant + Context для теми
function Button({ variant, children }: { variant: string; children: React.ReactNode }) {
return <button className={`btn-${variant}`}>{children}</button>;
}Ланцюжок ThemedButton -> PrimaryButton -> DangerButton виглядає охайно до першої зміни базового класу. Потім ламається все.
Помилка: очікування що дочірній компонент отримає дані автоматично
// Неправильно: UserInfo не отримує дані
<Card><UserInfo /></Card>
// Виправлення: render props коли дочірньому потрібні дані батька
<Card renderUser={(user) => <UserInfo user={user} />} />Дочірні компоненти не успадковують стан батька. Дані передаються явно або через Context.
Помилка: виклик hooks у класовому компоненті
// Неправильно: неприпустимий виклик hook
class MyComponent extends React.Component {
render() {
const [count, setCount] = useState(0); // Помилка
}
}Hooks працюють тільки у функціональних компонентах. Для спільної логіки - виноси у custom hook і використовуй його в різних компонентах.
Де зустрічається в реальних проєктах
- Material-UI v5:
<Modal><Form /></Modal>обгортає будь-який контент без наслідуванняModal. - Next.js 14 app router:
<Layout><Page /></Layout>- стандартний патерн composition для лейаутів. - Chakra UI: compound-компоненти на зразок
<Box>, що приймають будь-який вміст. - React Aria:
<Dialog>приймає будь-який контент черезchildren.
Питання на співбесіді
Q: Чому в React є React.Component, якщо inheritance не рекомендується?
A: React.Component - тонкий базовий клас для інтеграції з lifecycle. Це єдиний дозволений випадок inheritance в React. Власні ієрархії понад це порушують принцип єдиної відповідальності.
Q: Покажи як ділити логіку між компонентами без inheritance.
A: Виноси у custom hook. useModal() повертає стан і обробники. Будь-який компонент може його викликати без жодного extends.
Q: Коли брати Context замість composition?
A: Composition явна: передаєш props напряму. Context підходить для даних, які потрібні багатьом компонентам на різних рівнях без prop drilling (тема, авторизація, локаль). Спочатку composition, Context - коли ланцюжки props стають завеликими.
Q: Що замінило mixins і чому вони були гірші за inheritance?
A: Hooks замінили mixins. Mixins патчили методи прямо в прототип компонента, що спричиняло колізії імен і робило код непрослідковуваним. Hooks компонують чисті функції без побічних ефектів. Це відповідь рівня senior, яку чекають на співбесіді.
Приклади
Базовий: картка з довільним вмістом
function Card({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="card">
<h3>{title}</h3>
<div className="card-body">{children}</div>
</div>
);
}
// Один Card, два різні варіанти вмісту
<Card title="Профіль">
<Avatar src={user.avatar} />
<UserInfo name={user.name} />
</Card>
<Card title="Налаштування">
<SettingsForm />
</Card>Один компонент Card. Два різні варіанти вмісту. Жодного extends.
Середній рівень: патерн слотів для лейауту сторінки
// Іменовані слоти через props - саме так працює app router у Next.js
function Layout({
header,
sidebar,
content,
}: {
header: React.ReactNode;
sidebar: React.ReactNode;
content: React.ReactNode;
}) {
return (
<div className="layout">
<header>{header}</header>
<aside>{sidebar}</aside>
<main>{content}</main>
</div>
);
}
// Кожен слот отримує свій компонент
<Layout
header={<Navbar user={currentUser} />}
sidebar={<Sidebar links={navLinks} />}
content={<DashboardPage />}
/>Layout не знає як виглядають Navbar, Sidebar або DashboardPage - лише розставляє їх. Я використовую цей патерн у кожному серйозному React-проєкті: він добре тримається коли дизайн змінюється, бо кожен слот можна замінити незалежно.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.