Комментарий пользователя
Здравствуйте! В своих эффектах или колбэках я иногда замечаю, что код работает с устаревшими значениями состояния. Например, setInterval может читать старый state, а в колбэках используются неактуальные переменные. В чем причины такой проблемы и как писать код, чтобы всегда использовать актуальные значения состояния?
Ответ специалиста
Здравствуйте, Регина! В React есть распространенная и тонкая проблема, называемая stale closure (устаревшее замыкание). Суть в том, что замыкание «запоминает» значения переменных на момент создания функции. Если вы создаете эффект или колбэк один раз, а состояние потом меняется, внутренняя логика продолжит работать со старыми значениями.
Решения зависят от ситуации. Можно пересоздавать функцию или эффект с нужными зависимостями, использовать функциональный вариант setState для обновления состояния, либо хранить актуальные данные в ref и читать их внутри длительно живущих callbacks.
Ниже представлен пример классической проблемы stale closure — когда setInterval читает старое состояние:
import React, { useState, useEffect } from 'react';
function TimerBroken() {
const [count, setCount] = useState(0);
useEffect(() => {
// Это замыкание захватит count === 0 и будет логировать 0 постоянно
const id = setInterval(() => {
console.log('count (broken):', count);
}, 1000);
return () => clearInterval(id);
}, []); // пустые deps — эффект создается один раз
// ...
}
Если не критично частое пересоздание, просто указывайте нужные значения в массиве зависимостей у useEffect — эффект будет пересоздаваться при их изменении.
import React, { useState, useEffect } from 'react';
function TimerRecreate() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log('count (recreate):', count);
}, 1000);
return () => clearInterval(id);
}, [count]); // эффект пересоздается, захватывая актуальное count
// Но учтите: интервал будет сбрасываться при каждом изменении count
}
Если нужно, чтобы обработчик или таймер жили постоянно и не пересоздавались, держите актуальные данные в useRef и читайте их из замыкания внутри эффекта:
import React, { useState, useEffect, useRef } from 'react';
function TimerRef() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
// синхронизируем ref с актуальным состоянием
useEffect(() => { countRef.current = count; }, [count]);
useEffect(() => {
const id = setInterval(() => {
// читаем актуальное значение из ref
console.log('count (via ref):', countRef.current);
}, 1000);
return () +; clearInterval(id);
}, []); // эффект создаем один раз, но внутри читаем актуальный ref
}
Если задача — обновить состояние на основе прошлого значения, используйте функциональную форму setState (гарантированно работает без stale issues):
function Counter() {
const [count, setCount] = useState(0);
function incrementTwiceWrong() {
// может привести к "старому" count
setCount(count + 1);
setCount(count + 1);
}
function incrementTwiceRight() {
// корректно увеличит на 2
setCount(prev => prev + 1);
setCount(prev => prev + 1);
}
}
Если ваш обработчик или таймер должен жить постоянно и не перезапускаться, лучше хранить актуальные данные в ref и читать их прямо оттуда. Если же логика должна реагировать на изменения и обновляться, перечисляйте нужные зависимости в массиве deps эффекта или мемоизируйте callback с этими зависимостями. Для обновления состояния на основе предыдущего всегда используйте функциональную форму setState(prev => ...).
Если в DevTools вы заметили, что console.log внутри таймера или callback выводит старые значения — примените один из этих паттернов. Выбирайте подход, исходя из того, как именно должен себя вести код.
Хотите писать код увереннее? Получайте советы и разборы в рассылке Академии Selectel.