Как написать кастомный хук useState с колбэком при смене состояния в React
Что проверяют
Понимаете ли вы, почему у сеттера из useState нет второго аргумента, как у this.setState в классах, и когда на самом деле применяется новое состояние. Следом обычно просят найти баг в наивной реализации через useEffect.
Как отвечать
Объясните модель: вызов сеттера не меняет переменную сразу, а ставит обновление в очередь. React пакетно применяет его на следующем рендере, и новое значение видно только там. Поэтому «после смены» значит «после коммита», а это место для эффекта.
Рабочая реализация строится так:
- колбэк на конкретный вызов: хук возвращает
setState(value, cb), который кладётcbвuseRefи вызывает обычный сеттер; - вызов после коммита:
useEffectс зависимостью[state]достаёт колбэк из ref, вызывает его с новым значением и очищает ref; - без срабатывания на монтировании: колбэк вызывается, только если он был сохранён, поэтому первый рендер ничего не запускает;
- нет изменения, нет колбэка: если новое значение равно старому по
Object.is, React не перерисует компонент и эффект не сработает. Это поведение стоит либо принять, либо описать в контракте хука; - нужен DOM до отрисовки: тогда
useLayoutEffectвместоuseEffect, например чтобы измерить элемент.
Если колбэк передаётся один на весь хук, его держат в ref, который обновляется на каждом рендере, или оборачивают в useEffectEvent, стабильный с React 19.2. Тогда эффект зависит только от state.
Сильный ход: сказать, что часто такой хук не нужен. Если новое значение вычисляется в обработчике, проще посчитать его в переменную, вызвать сеттер и сразу сделать побочное действие с этой же переменной.
Где спотыкаются
Кладут инлайн-колбэк в зависимости эффекта. Функция новая на каждом рендере, эффект срабатывает при любой перерисовке, а если колбэк сам меняет состояние, получается бесконечный цикл.
Читают count сразу после setCount(count + 1) и удивляются старому значению. Это замыкание текущего рендера, а не баг React.
Забывают, что наивный эффект вызывает колбэк ещё на монтировании, а в StrictMode при разработке эффект прогоняется дважды.