Skip to main content

Композиція проти наслідування в 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.

Швидкий приклад

tsx
// 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 завжди.

Таблиця порівняння

АспектCompositionInheritance
ГнучкістьДовільна комбінація компонентівФіксований ланцюжок класів
ПеревикористанняОбгортання будь-яких UI-елементівТільки прямі нащадки
Підтримка кодуНезалежна зміна компонентівЗміна базового класу ламає все
Підтримка ReactОфіційний патерн, hooks-friendlyНе рекомендовано після React.Component
Коли використовувати99% випадківТільки legacy-код з класами

Типові помилки

Помилка: власні ієрархії класів

tsx
// Неправильно: крихкий ланцюжок 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 виглядає охайно до першої зміни базового класу. Потім ламається все.

Помилка: очікування що дочірній компонент отримає дані автоматично

tsx
// Неправильно: UserInfo не отримує дані <Card><UserInfo /></Card> // Виправлення: render props коли дочірньому потрібні дані батька <Card renderUser={(user) => <UserInfo user={user} />} />

Дочірні компоненти не успадковують стан батька. Дані передаються явно або через Context.

Помилка: виклик hooks у класовому компоненті

tsx
// Неправильно: неприпустимий виклик 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, яку чекають на співбесіді.

Приклади

Базовий: картка з довільним вмістом

tsx
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.

Середній рівень: патерн слотів для лейауту сторінки

tsx
// Іменовані слоти через 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-проєкті: він добре тримається коли дизайн змінюється, бо кожен слот можна замінити незалежно.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?