Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке HOC (компонент вищого порядку) у React». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**HOC (Higher-Order Component, компонент вищого порядку)** — це функція, яка приймає React-компонент і повертає новий компонент з додатковою логікою. ```jsx const withAuth = (WrappedComponent) => { function Authenticated(props) { const { user } = useContext(AuthContext); if (!user) return <Navigate to="/login" />; return <WrappedComponent {...props} user={user} />; } return Authenticated; }; const ProtectedDashboard = withAuth(Dashboard); ``` **Головне:** оригінальний компонент не змінюється. У сучасному React кастомні хуки вирішують ті самі задачі з меншою кількістю обгорток.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**HOC (Higher-Order Component, компонент вищого порядку)** — це функція, яка приймає React-компонент і повертає новий компонент з додатковою логікою, не зачіпаючи оригінал. ## Теорія ### TL;DR - HOC = функція, яка бере компонент і повертає покращений - Аналогія: передаєш звичайний блендер, отримуєш такий, що сам вимірює інгредієнти перед запуском - Головне: логіка додається через композицію, а не через зміну вихідного коду - Реальні приклади: `connect` у Redux, `withRouter` у React Router, `withStyles` у Material-UI - Правило вибору: HOC підходить для класових кодобаз; у новому функціональному коді зазвичай зручніші кастомні хуки ### Швидкий приклад ```jsx // withLoading: додає спінер без будь-яких змін у UserList const withLoading = (WrappedComponent) => (props) => { if (props.isLoading) return <div>Loading...</div>; // перехоплює до рендеру return <WrappedComponent {...props} />; // передає всі пропси далі }; const UserList = ({ users }) => ( <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul> ); const UserListWithLoading = withLoading(UserList); // <UserListWithLoading isLoading={false} users={[{id:1, name:'Alice'}]} /> // Результат: <ul><li>Alice</li></ul> ``` `withLoading` нічого не знає про `UserList`. Перевіряє один проп і передає решту далі. Саме в цьому і є сенс. ### Головна різниця: композиція проти наслідування HOC — це композиція. Ти обгортаєш компонент ззовні, зберігаєш його оригінальний рендер і додаєш логіку через замикання (closure). Оригінальний компонент не змінюється. Наслідування, навпаки, лізе всередину класу і модифікує його — це ламається в concurrent mode React і ускладнює відлагодження. З HOC-ами можна стекати шари, знімати їх і тестувати кожен окремо. ### Коли використовувати - Спільне отримання даних у багатьох компонентах -> HOC `withDataFetch` - Перевірка авторизації в класовій кодобазі -> HOC; у новому функціональному коді простіше кастомний хук - Логування або телеметрія під час рендеру -> HOC обгортає сам виклик рендеру без накладних витрат хуків - Логіка потрібна лише одному компоненту -> не роби HOC, вбудуй її напряму ### Як React опрацьовує HOC зсередини React сприймає повернуту функцію як звичайний компонент. При монтуванні запускає зовнішню функцію HOC, яка створює замикання (closure) над `WrappedComponent`. Обгортка захоплює пропси у своєму lifecycle і делегує рендер через `React.createElement`. Жодних спеціальних браузерних API. Це звичайне функціональне програмування, яке виконується під час reconciliation. ### Типові помилки **Мутація переданого компонента:** ```jsx // Неправильно: змінює оригінальний компонент const badHOC = (WC) => { WC.prototype.newMethod = () => {}; // забруднює джерело return WC; }; ``` Це порушує чистоту компонента і ламається в StrictMode, де React двічі монтує компоненти, щоб знайти такі побічні ефекти. Повертай новий компонент — ніколи не модифікуй отриманий. **Пропущений displayName:** ```jsx // Неправильно: у React DevTools показує "Anonymous" const withData = (WC) => (props) => <WC {...props} />; ``` ```jsx // Правильно const withData = (WC) => { const Enhanced = (props) => <WC {...props} />; Enhanced.displayName = `withData(${WC.displayName || WC.name})`; return Enhanced; }; ``` Без `displayName` DevTools показує `Anonymous^12`. Дебажити таке вкрай незручно. **HOC всередині render:** ```jsx // Неправильно: новий екземпляр HOC при кожному рендері function App() { const UserListWithLogger = withLogger(UserList); // ремонтується при кожному рендері return <UserListWithLogger users={data} />; } ``` Компонент розмонтовується і монтується заново при кожному рендері, втрачаючи весь локальний стан. Оголошуй HOC на рівні модуля, поза будь-яким компонентом. **Зникнення статичних методів:** Коли обгортаєш компонент, статичні методи оригіналу зникають. Копіюй їх вручну: ```jsx EnhancedComponent.staticMethod = WrappedComponent.staticMethod; ``` ### Де зустрічається в реальних проєктах - Redux: `connect(mapStateToProps)(Component)` підписується на стор і передає стан як пропси - React Router v5: `withRouter` додає `location`, `history`, `match` до класових компонентів - Material-UI v4: `withStyles` і `withTheme` прикріплюють CSS-in-JS стилі - Композиція кількох HOC: `withAuth(withData(withLogger(Dashboard)))` або `compose(withAuth, withData, withLogger)(Dashboard)` через `flowRight` з lodash ### Питання на співбесіді **Q:** Напиши HOC, який отримує дані і передає їх як проп. **A:** Дивись приклад `withFetch` нижче. Senior-відповідь додає обробку помилок, стан завантаження і `AbortController` для скасування запиту при демонтажі. **Q:** Як компонувати кілька HOC? **A:** `withAuth(withData(Component))` або `compose(withAuth, withData)(Component)`. Зовнішній HOC запускається першим під час рендеру. **Q:** Чому HOC-и вже не такі популярні? **A:** Хуки вирішують ті самі задачі без стеку обгорток, колізій пропсів і проблем з передачею ref. На React 16.8+ кастомний хук майже завжди простіший. **Q:** HOC проти render props — в чому різниця? **A:** HOC автоматично додає пропси і повертає готовий компонент — потік даних неявний. Render props передають дані через функцію в `children`, де все видно явно. Обидва підходи працюють; render props зручніші, коли потрібен точний контроль над тим, що рендериться. **Q:** Як тестувати компонент, обгорнутий у HOC? **A:** Тестуй HOC і обгорнутий компонент окремо. Мокай залежності HOC, рендер обгортки — і перевіряй, які пропси були передані. Або виноси логіку у кастомний хук і тестуй хук напряму. **Q:** Як реф поводиться в HOC? (Senior) **A:** Реф не проходить крізь HOC автоматично, бо `ref` не є звичайним пропом. Використовуй `React.forwardRef` всередині HOC. Без нього `ref` буде `null` на зовнішній обгортці і будь-який імперативний handle зламається. ## Приклади ### Базовий: HOC для логування ```jsx function withLogger(WrappedComponent) { function LoggerComponent(props) { console.log(`[${WrappedComponent.name}] props:`, props); // логує при кожному рендері return <WrappedComponent {...props} />; } LoggerComponent.displayName = `withLogger(${WrappedComponent.name})`; return LoggerComponent; } const Hello = ({ name }) => <h1>Hello, {name}</h1>; const HelloWithLogger = withLogger(Hello); // <HelloWithLogger name="Alice" /> // Консоль: [Hello] props: { name: 'Alice' } // Результат: <h1>Hello, Alice</h1> ``` Простий кейс: без стану, без побічних ефектів. HOC просто обгортає і логує. `displayName` виставлений — DevTools покаже `withLogger(Hello)`, а не `Anonymous`. ### Середній рівень: HOC для авторизації ```jsx import { useContext } from 'react'; import { Navigate } from 'react-router-dom'; const withAuth = (WrappedComponent) => { function Authenticated(props) { const { user } = useContext(AuthContext); // хуки всередині HOC працюють з React 16.8+ if (!user) return <Navigate to="/login" />; // редирект без авторизації return <WrappedComponent {...props} user={user} />; // передаємо user як проп } Authenticated.displayName = `withAuth(${WrappedComponent.displayName || WrappedComponent.name})`; return Authenticated; }; const Dashboard = ({ user }) => <div>Ласкаво просимо, {user.name}!</div>; const ProtectedDashboard = withAuth(Dashboard); // <ProtectedDashboard /> без user -> редирект на /login // <ProtectedDashboard /> з user -> "Ласкаво просимо, Alice!" ``` Цей патерн схожий на те, що робить `react-redux-auth-wrapper` зсередини. `Dashboard` нічого не знає про логіку авторизації — HOC повністю бере це рішення на себе. ### Просунутий рівень: HOC для отримання даних з очищенням ```jsx import { useState, useEffect } from 'react'; function withFetch(WrappedComponent, url) { function ComponentWithData(props) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const controller = new AbortController(); // скасовуємо запит при демонтажі fetch(url, { signal: controller.signal }) .then((res) => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }) .then(setData) .catch((err) => { if (err.name !== 'AbortError') setError(err.message); }) .finally(() => setLoading(false)); return () => controller.abort(); // cleanup при демонтажі }, []); if (loading) return <div>Завантаження...</div>; if (error) return <div>Помилка: {error}</div>; return <WrappedComponent {...props} data={data} />; } ComponentWithData.displayName = `withFetch(${WrappedComponent.displayName || WrappedComponent.name})`; return ComponentWithData; } const UserList = ({ data }) => ( <ul>{data?.map(u => <li key={u.id}>{u.name}</li>)}</ul> ); const UserListWithData = withFetch(UserList, '/api/users'); // <UserListWithData /> -> завантажує /api/users, показує стан завантаження, потім список ``` `AbortController` — це те, що відрізняє продакшн HOC від навчального прикладу. Я бачив, як ця помилка викликає memory warnings у дашборд-застосунках з частою навігацією між роутами. Без нього fetch, який завершується після демонтажу, намагається викликати `setData` на вже знищеному компоненті. Отримуєш попередження і потенційний витік пам'яті.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.