Skip to main content

Правила використання хуків у React

Правила використання хуків у 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 завжди рахує два слоти. Поміняй порядок і правило буде порушено одразу.

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

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

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

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