Зачем в setState передавать callback вместо значения
Что проверяют
Понимаете ли вы модель обновления состояния в React: состояние в рендере это снимок, а обновления ставятся в очередь. Следом часто просят объяснить, почему три вызова setCount(count + 1) подряд увеличивают счётчик на единицу, и что такое устаревшее замыкание.
Как отвечать
Суть в том, откуда берётся предыдущее значение:
- передаёте значение:
setCount(count + 1)берётcountиз замыкания текущего рендера. Три таких вызова в одном обработчике трижды запишут одно и то же число; - передаёте функцию-апдейтер:
setCount(c => c + 1). React кладёт функцию в очередь и на следующем рендере вызывает её с результатом предыдущих обновлений. Три вызова дадут плюс три; - в классовых компонентах то же самое:
this.setState(prev => ({ count: prev.count + 1 })). Не путайте со вторым аргументомsetState(obj, callback): это другой колбэк, он вызывается после применения обновления, а в хуках его роль играетuseEffect.
Главный практический случай это эффекты и таймеры. setInterval(() => setCount(count + 1), 1000) в useEffect с пустым массивом зависимостей навсегда увидит начальный count и застынет на единице. С апдейтером setCount(c => c + 1) зависимость от count не нужна и интервал не пересоздаётся. Апдейтер помогает и в useCallback: колбэк не меняет ссылку при каждом изменении состояния.
С React 18 обновления батчатся автоматически везде, в том числе в промисах и setTimeout, поэтому рассчитывать, что состояние обновится сразу после вызова, нельзя нигде. Если новое значение не зависит от предыдущего, например setOpen(false), апдейтер не нужен.
Где спотыкаются
Говорят, что setState асинхронный, и пытаются сделать await. Он не возвращает промис: он планирует рендер, а переменная в текущем рендере не изменится никогда.
Мутируют состояние внутри апдейтера: prev.items.push(x); return prev. Ссылка та же, React сравнивает через Object.is и может пропустить рендер.
Кладут в апдейтер побочные эффекты: запрос, логирование, запись в localStorage. Функция должна быть чистой, а в StrictMode при разработке React специально вызывает её дважды, чтобы это вскрыть.