Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Правила використання хуків у React». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Правила використання хуків у React** вимагають викликати хуки лише на верхньому рівні компонента або кастомного хука, і лише з функціональних компонентів або кастомних хуків, а не зі звичайних функцій чи обробників. ```tsx // Неправильно if (condition) { useState(0); } // Правильно const [count, setCount] = useState(0); if (condition) { /* використати count */ } ``` **Ключове:** React відстежує хуки за порядком виклику. Однаковий порядок кожен рендер = стабільний стан.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Правила використання хуків у React** визначають де і як можна викликати функції на зразок `useState` та `useEffect`. Порушиш їх і React втрачає зв'язок між станом і компонентом. ## Теорія ### TL;DR - Хуки викликаються лише на верхньому рівні компонента або кастомного хука, ніколи всередині умов, циклів чи вкладених функцій - Хуки можна викликати лише з функціональних компонентів або кастомних хуків (функцій з префіксом `use`) - React відстежує хуки за позицією виклику, а не за іменем. Однаковий порядок кожен рендер = стабільний стан - Аналогія: ресторан готує страви у фіксованій послідовності. Пропусти один крок і кухня загубить твоє замовлення - Правило вибору: спочатку всі виклики хуків, умови і цикли - нижче ### Швидкий приклад ```tsx // ПОГАНО: умовний виклик хука ламає порядок function BadComponent({ showCount }: { showCount: boolean }) { if (showCount) { const [count, setCount] = useState(0); // React губить відстеження слотів } return <div>{showCount ? count : 'Приховано'}</div>; } // ДОБРЕ: хук завжди викликається, умова - після function GoodComponent({ showCount }: { showCount: boolean }) { const [count, setCount] = useState(0); // Слот 1 - завжди викликається return <div>{showCount ? count : 'Приховано'}</div>; } ``` `BadComponent` кидає "Rendered more hooks than during the previous render". `GoodComponent` працює стабільно при кожному рендері. ### Чому порядок виклику важливий React зберігає стан хуків у зв'язаному списку (linked list), прикріпленому до fiber-вузла кожного компонента. При першому рендері будується цей список: слот 1, слот 2, слот 3. При кожному наступному рендері React проходить по ньому в тому ж порядку і відновлює кожне значення. Якщо умова пропускає слот 1, слот 2 отримує дані з слота 1 і все зсувається. Правило не є довільним обмеженням. Це пряме наслідування архітектури fiber у React. ### Коли використовувати - **Потрібен стан або side effect у компоненті** - додай хук на верхньому рівні, до будь-якого `return` - **Одна й та сама логіка зі станом у кількох компонентах** - винеси в кастомний хук `useMyLogic`, не в звичайну функцію - **Потрібна умовна логіка** - помісти умову всередину виклику хука або після всіх хуків - **Цикли** - виклик одного хука поза циклом, значення зберігай у масиві або об'єкті з ключем-індексом - **Обробники подій** - визначай їх всередині компонента, але ніколи не викликай хуки в тілі самого обробника Ранній `return` дозволений. `if (!userId) return null` після викликів `useState` - валідний React 18 код. Хуки просто мають завершитись до будь-яких умовних виходів. ### Як React виявляє порушення Внутрішній диспетчер React перемикається між фазами `mount` і `update` при кожному рендері. Під час `update` він перевіряє, що кількість викликів хуків збігається з попереднім рендером. Невідповідність одразу призводить до помилки з `ReactCurrentDispatcher`. `eslint-plugin-react-hooks` перехоплює більшість порушень статично, ще до запуску, тому його включають у стандартне налаштування будь-якого React-проєкту. ### Типові помилки **Хук всередині умови** ```tsx if (isLoggedIn) { const [user, setUser] = useState(null); // зсуває всі наступні слоти } ``` Виправлення: спочатку виклик хука без умов, перевірку `isLoggedIn` - нижче. **Хук всередині обробника події** ```tsx function handleClick() { const [local, setLocal] = useState(0); // звичайна JS-функція, React її не відстежує } ``` Виправлення: перенеси стан на рівень компонента. Для мемоізації обробника використовуй `useCallback`. **Хук всередині циклу** ```tsx items.map(item => { const [active, setActive] = useState(false); // нові екземпляри при кожному рендері return <li>{active ? 'увімк' : 'вимк'}</li>; }); ``` Виправлення: один `useState` вгорі, значення зберігай в об'єкті з ключем-id елемента. **Кастомний хук без префікса `use`** ```tsx function myFetchLogic() { const data = useSWR('/api/data'); // ESLint не відстежить порушення всередині } ``` Виправлення: перейменуй у `useMyFetchLogic`. Префікс `use` сигналізує і React, і ESLint, що це хук. **Хук поза компонентом** ```tsx const globalCount = useState(0); // область модуля, не відстежується ``` Виправлення: перенеси всередину компонента або кастомного хука. ### Де зустрічається у реальних проєктах - React: `useState` і `useEffect` у кожному функціональному компоненті починаючи з React 16.8 - Next.js: `useSWR` від Vercel для отримання даних у app router - Redux Toolkit: `useSelector` і `useDispatch` у підключених компонентах - TanStack Query: `useQuery` і `useMutation`, понад 2 мільйони завантажень на тиждень - Zustand: `useStore` для легковагового глобального стану На практиці помилку "хук всередині `.map()`" найчастіше бачу в код-рев'ю команд, що мігрують з класових компонентів. Виправлення завжди одне: один хук, один масив поза циклом. ### Follow-up питання **Q:** Чому React відстежує хуки за порядком виклику, а не за іменем або ID? **A:** Імена не унікальні. Два виклики `useState` в одному компоненті зіштовхнулись би. Позиція виклику дає унікальний стабільний слот без додаткових метаданих. **Q:** Чи можна викликати хук після раннього `return`? **A:** Ні. Хук після `return` пропускається при цьому рендері і кількість слотів не збігається. Всі хуки повинні відпрацювати до будь-яких умовних виходів. **Q:** Що таке кастомний хук і коли його варто створювати? **A:** Функція з префіксом `use`, яка викликає інші хуки всередині. Створюй, коли одна й та сама логіка зі станом повторюється у двох або більше компонентах. **Q:** Як `eslint-plugin-react-hooks` визначає де викликаються хуки? **A:** Він аналізує AST і позначає будь-який виклик функції з `use` всередині умови, циклу або функції, яка не є компонентом. Також перевіряє наявність префікса `use` у кастомних хуків. **Q (senior):** Що відбувається у fiber React, коли порядок хуків змінюється між рендерами? **A:** Кожен fiber зберігає зв'язаний список `memoizedState`. При оновленні React читає вузли послідовно. Відсутній вузол означає, що поточний вузол містить дані з попереднього. Всі наступні читання зсунуті на один слот. React виявляє невідповідність кількості через `ReactCurrentDispatcher` і кидає помилку до повернення некоректного стану. ## Приклади ### Лічильник з двома слотами стану ```tsx function Counter() { const [count, setCount] = useState(0); // Слот 1 const [label, setLabel] = useState('кліків'); // Слот 2 - завжди викликається return ( <div> <p>{count} {label}</p> <button onClick={() => setCount(c => c + 1)}>+1</button> </div> ); } // Виведення: стабільні count і label між рендерами. Слоти ніколи не зсуваються. ``` Обидва хуки викликаються при кожному рендері. React відновлює `count` у слот 1 і `label` у слот 2 без плутанини. ### Список задач з фільтрацією ```tsx function TodoList({ todos }: { todos: { id: number; text: string; done: boolean }[] }) { const [filter, setFilter] = useState<'all' | 'active' | 'done'>('all'); // Слот 1 const visibleTodos = useMemo( () => filter === 'all' ? todos : todos.filter(t => filter === 'done' ? t.done : !t.done), [todos, filter] ); // Слот 2 return ( <div> <select onChange={e => setFilter(e.target.value as any)} value={filter}> <option value="all">всі</option> <option value="active">активні</option> <option value="done">виконані</option> </select> <ul>{visibleTodos.map(t => <li key={t.id}>{t.text}</li>)}</ul> </div> ); } // Виведення: стан фільтра зберігається між рендерами. Memo пропускає перерахунок якщо вхідні дані не змінились. ``` `useState` у слоті 1, `useMemo` у слоті 2, жодних умов навколо викликів. Логіка фільтрації - всередині `useMemo`, а не навколо самого хука. ### Розширений: ранній return після хуків ```tsx function Profile({ userId }: { userId: string | null }) { const [profile, setProfile] = useState(null); // Слот 1 - виконується при кожному рендері const [posts, setPosts] = useState([]); // Слот 2 - виконується при кожному рендері if (!userId) return <div>Користувача не вибрано</div>; // ранній return тут дозволений // логіка завантаження і рендерингу коли userId є return <div>Профіль {userId}</div>; } // Виведення: кількість слотів не змінюється, бо обидва useState відпрацьовують до умовного виходу. ``` Обидва `useState` виконуються до перевірки `if (!userId)`, тому React завжди рахує два слоти. Поміняй порядок і правило буде порушено одразу.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.