Как на самом деле работают React Hooks

#react#hooks#frontend

Когда в 2018 году вышли хуки, документация React уверяла: “они просто работают”. Но когда в продакшене начали всплывать баги типа “Rendered fewer hooks than expected”, стало ясно — под капотом происходит что-то нетривиальное.

Хуки — это не магия, а call stack

Первый миф, который нужно развенчать: хуки не привязаны к компоненту. На самом деле React опирается на порядок вызовов. Вот что происходит при рендере:

  1. React начинает выполнение функционального компонента
  2. Каждый вызов useState/useEffect помещает данные в стек
  3. После рендера стек сверяется с предыдущим состоянием
function BuggyComponent() {
  if (Math.random() > 0.5) {
    useState(); // Хук 1
  }
  useState(); // Хук 2
}

Этот код будет падать, потому что между рендерами меняется количество хуков. React хранит состояние в виде связного списка, где каждый хук знает о следующем.

Почему хуки работают только на верхнем уровне

Правило “не вызывайте хуки внутри условий” — не прихоть React Team, а следствие реализации. Представьте:

// Первый рендер
useState('A'); // Запоминаем как hook 1
useState('B'); // hook 2

// Второй рендер
if (condition) {
  useState('A'); // hook 1
}
// Где hook 2? Ломаем всю цепочку

Вот как это выглядит в псевдокоде React:

let currentHook = 0;
let hooks = [];

function useState(initial) {
  hooks[currentHook] = hooks[currentHook] || initial;
  const setStateHookIndex = currentHook;
  
  function setState(value) {
    hooks[setStateHookIndex] = value;
    render();
  }
  
  return [hooks[currentHook++], setState];
}

Подводные камни замыканий

Самый коварный баг — “stale closure” в useEffect. Проблема в том, что эффекты захватывают значения на момент создания:

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      console.log(count); // Всегда 0!
    }, 1000);
    return () => clearInterval(id);
  }, []); // Пустой массив зависимостей

  return <button onClick={() => setCount(c => c + 1)}>+</button>;
}

Решение — либо добавлять count в зависимости, либо использовать функциональную форму setCount.

Хуки и текущий стек технологий

С появлением Server Components модель хуков усложнилась. Теперь нужно учитывать:

Вот как выглядит типичный баг при миграции:

'use client'; // Без этой директивы хуки не работают

function ClientComponent() {
  const [state] = useState(); // Теперь ок
  return <div>{state}</div>;
}

Что пробовать в 2024

  1. use hook (экспериментальный) — попытка сделать хуки более гибкими
  2. React Forget — компилятор, который может устранить need for useMemo/useCallback
  3. Библиотеки типа usehooks-ts с готовыми паттернами

Хуки — это мощно, но не бесплатно. Их абстракция протекает в сложных сценариях, и понимание механики спасает от часов дебагга. Главное правило: если хук ведёт себя странно, ищите изменение порядка вызовов.


Источник: https://youtu.be/FneS7tCWBMU