Как React отслеживает количество вызовов хуков между рендерами?
Что проверяют
Понимаете ли вы, что у хука нет имени или ключа и React различает хуки только по порядку вызова. Из этого напрямую следуют правила хуков. Следом обычно спрашивают, что будет, если вызвать хук внутри if, и почему на use это правило не распространяется.
Как отвечать
Каркас механизма:
- где хранится: у каждого экземпляра компонента есть fiber, а в его поле
memoizedStateлежит односвязный список объектов хуков. УuseStateтам текущее значение и очередь обновлений, уuseEffectэффект и зависимости, уuseRefобъект ссылки; - первый рендер: каждый вызов хука создаёт новый узел и добавляет его в конец списка;
- повторный рендер: React идёт по существующему списку указателем. Первый вызов
useStateполучает первый узел, второй вызов второй узел, и так далее. Имя переменной[count, setCount]React не видит вообще; - проверка количества: если хуков вызвано меньше, чем в прошлый раз, React бросает ошибку «Rendered fewer hooks than expected», если больше, то «Rendered more hooks than during the previous render». В dev-сборке он ещё сверяет типы хуков по позициям и предупреждает о смене порядка.
Отсюда правила: хуки вызываются только на верхнем уровне компонента или кастомного хука, без условий, циклов и ранних return перед ними. Кастомный хук своего списка не создаёт, его внутренние хуки встраиваются в список вызвавшего компонента. За правилами следит eslint-plugin-react-hooks с правилом rules-of-hooks.
Добавьте про use из React 19: его можно вызывать в условиях и циклах, потому что он не заводит узел в этом списке. Контекст он читает напрямую, а промисы отслеживает отдельным механизмом.
Где спотыкаются
Говорят, что React сопоставляет состояние по имени переменной или по порядку объявления в коде. Решает только порядок вызова в конкретном рендере.
Считают ранний return безопасным, раз хук «всё равно написан ниже». Именно выход до хука и даёт «Rendered fewer hooks than expected».
Думают, что список хуков общий для компонента как функции. Он свой у каждого экземпляра: два <Counter /> на странице держат два независимых списка, поэтому их состояние не смешивается.