Почему в callbacks в React используются старые значения?

Вопрос: почему в callbacks в React используются «старые» значения?

Линия поддержки
Линия поддержки Ответы на вопросы пользователей
28 октября 2025

Код работает с устаревшими значениями состояния. Как писать так, чтобы всегда использовать актуальные значения состояния?

Изображение записи

Комментарий пользователя

Здравствуйте! В своих эффектах или колбэках я иногда замечаю, что код работает с устаревшими значениями состояния. Например, 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.