Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке кастомні хуки в React». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Кастомні хуки** - це JavaScript-функції з префіксом "use", які викликають вбудовані хуки React для спільного використання логіки зі станом у різних компонентах. ```jsx function useUser(userId) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { fetch(`/api/users/${userId}`).then(r => r.json()).then(setUser) .finally(() => setLoading(false)); }, [userId]); return { user, loading }; } ``` **Ключове:** префікс "use" сигналізує React застосовувати правила хуків: виклик тільки на верхньому рівні, не всередині умов чи колбеків.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Кастомні хуки** - це JavaScript-функції з префіксом "use", які викликають вбудовані хуки React для спільного використання логіки зі станом у різних компонентах. ## Теорія ### TL;DR - Кастомний хук схожий на рецепт у спільній книзі: написав `useState` + `useEffect` один раз, і будь-який компонент бере це без переписування - Головна різниця від звичайної функції: хук виконується під час фази рендеру React, тому стан і ефекти залишаються синхронізованими з компонентом - Коли витягувати: якщо та сама хук-логіка з'являється в 2+ компонентах або виростає до ~20 рядків - Якщо функція не викликає жодного хука - вона просто функція. Префікс "use" їй не потрібен. ### Швидкий приклад ```jsx // До: логіка ресайзу дублюється в кожному компоненті function WindowSize() { const [width, setWidth] = useState(window.innerWidth); useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); return <div>Ширина: {width}px</div>; } // Після: виносимо в хук, використовуємо де треба function useWindowSize() { const [width, setWidth] = useState(window.innerWidth); useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); // очищення при анмаунті }, []); return width; } function WindowSize() { const width = useWindowSize(); // компонент залишається чистим return <div>Ширина: {width}px</div>; } ``` Ось і весь патерн. Хук іде назовні, компонент спрощується. ### Чим кастомний хук відрізняється від звичайної функції Звичайну функцію можна викликати де завгодно: в циклах, умовах, колбеках. Кастомний хук - ні. React відстежує виклики хуків у фіксованому порядку всередині fiber-вузла кожного компонента через зв'язаний список станів та ефектів. Виклич хук умовно - список зміститься, і React прочитає не той стан з не того слота. Саме тому важливий префікс "use". React бачить його і застосовує правила хуків: виклик тільки на верхньому рівні, тільки з компонентів або інших хуків. Функція без "use" не отримує такого контролю, а її внутрішній стан не збережеться між рендерами. ### Коли використовувати - Fetch + стани loading/error, що повторюються в кількох компонентах: кастомний хук - Логіка валідації форми та відправки, що спільна для кількох сторінок: кастомний хук - Синхронізація стану з localStorage: кастомний хук - Разовий форматтер дати без стану: звичайна функція - UI-анімація, яка є тільки в одному компоненті: залиш inline ### Як React обробляє кастомні хуки внутрішньо React обробляє кастомні хуки під час фази рендеру через той самий механізм, що й вбудовані хуки. Кожен виклик хука відповідає слоту в зв'язаному списку fiber-вузла через диспетчери `mountState` та `updateState`. Під час ре-рендеру React повторює виклики в тому ж порядку і відновлює стан з цих слотів. Жодної магії V8 тут немає - це reconciler React забезпечує порядок. Саме тому хуки вільно компонуються: `useUser` може викликати `useFetch`, який викликає `useState` і `useEffect`. Всі вони ділять один fiber компонента. ### Типові помилки **Забули префікс "use":** ```jsx function getWindowSize() { // React не застосовує правила хуків const [width, setWidth] = useState(window.innerWidth); // стан не збережеться між рендерами } ``` Без префікса React сприймає функцію як звичайну і пропускає відстеження стану. Перейменуй на `useWindowSize`. **Виклик хука умовно:** ```jsx function Component({ show }) { if (show) { const [data, setData] = useState(null); // порядок хуків змінюється при зміні show } } ``` Це призводить до "Invalid hook call" або непередбачуваних багів зі станом. Завжди винось виклик хука на верхній рівень функції. **Stale closure (застарілий стан) у fetch-хуці:** ```jsx function useAsyncTask(callback) { const [result, setResult] = useState(null); useEffect(() => { callback().then(setResult); }, [callback]); // callback має бути стабільним, інакше ефект спрацьовує щоразу return result; } // Неправильно: callback відтворюється на кожному рендері function BadComponent() { const [count, setCount] = useState(0); const result = useAsyncTask(async () => { return count; // завжди повертає count з першого рендеру }); } // Правильно: стабілізуй через useCallback const callback = useCallback(async () => { return count; }, [count]); const result = useAsyncTask(callback); ``` Це проблема номер один з кастомними хуками в продакшені. ESLint-правило `react-hooks/exhaustive-deps` ловить її автоматично. **Хук для тривіальної логіки:** ```jsx function useDouble(n) { return n * 2; } // всередині немає хуків, немає сенсу ``` Якщо функція не викликає жодного хука - вона просто функція. Префікс "use" тут зайвий. ### Де зустрічається - TanStack Query: `useQuery` компонує `useEffect` + `useState` для фетчингу з кешуванням - React Hook Form: `useForm` інкапсулює весь стан форми та валідацію в один виклик - SWR (використовується в Next.js): `useSWR` обробляє fetch, кеш і ревалідацію - Zustand: `useStore` як легкий хук для глобального стану Кастомні хуки замінили HOC (higher-order components) та render props для спільного використання логіки зі станом. ### Питання на співбесіді **Q:** Чому ім'я кастомного хука має починатись з "use"? **A:** React шукає префікс "use" під час рендеру, щоб застосувати правила хуків. Без нього функція вважається звичайною, стан не зберігається між рендерами, а ефекти не спрацьовують коректно. **Q:** Чи може кастомний хук викликати інші кастомні хуки? **A:** Так. Хуки вільно компонуються. `useUser` може викликати `useFetch`, який викликає `useState` і `useEffect`. Всі вони ділять fiber одного компонента. **Q:** Що станеться, якщо викликати кастомний хук всередині колбека? **A:** React викине "Invalid hook call". Хуки мають викликатись тільки на верхньому рівні функціонального компонента або іншого хука, але не всередині обробників подій чи async-функцій. **Q:** В чому різниця між кастомним хуком і `useReducer`? **A:** `useReducer` управляє складним локальним станом в одному компоненті. Кастомний хук упаковує логіку стану так, щоб її могли спільно використовувати кілька компонентів. **Q:** Поясни баг зі stale closure у fetch-хуці (рівень senior). **A:** Якщо масив залежностей `useEffect` не містить пропсу, наприклад `userId`, ефект захоплює значення з першого рендеру. Коли компонент отримає новий `userId`, ефект не перезапуститься і відобразяться дані для старого користувача. Рішення: включи всі залежності в масив або стабілізуй колбек через `useCallback`. ## Приклади ### Базовий: відстеження ширини вікна ```jsx function useWindowSize() { const [width, setWidth] = useState(window.innerWidth); useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); // очищення при анмаунті }, []); return width; } function Header() { const width = useWindowSize(); return <nav>{width < 768 ? <MobileMenu /> : <DesktopMenu />}</nav>; } ``` Один хук, багато компонентів. Будь-який компонент, якому потрібна ширина вікна, викликає `useWindowSize()` замість того, щоб заново описувати логіку слухача. ### Середній: фетчинг даних зі станами loading і error ```jsx // useUser.js - використовується в дашбордах, профілях, адмін-панелях function useUser(userId) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { setLoading(true); fetch(`/api/users/${userId}`) .then(res => res.json()) .then(setUser) .catch(setError) .finally(() => setLoading(false)); }, [userId]); // автоматично перефетчить при зміні userId return { user, loading, error }; } function Profile({ userId }) { const { user, loading, error } = useUser(userId); if (loading) return <div>Завантаження...</div>; if (error) return <div>Не вдалось завантажити користувача</div>; return <div>{user.name}</div>; } ``` Компонент відповідає тільки за відображення. Вся логіка фетчингу живе в хуку. Я бачив, як відсутній `userId` у масиві залежностей коштував кількох годин дебагу в продакшені: UI показував дані старого користувача при переключенні між профілями. Один запис у dependency array вирішує це.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.