React State: когда rerender убивает локальные переменные и как с этим жить

#react#state#rerender

Один из самых частых когнитивных диссонансов у начинающих React-разработчиков — поведение локальных переменных при rerender. Вопрос на Reddit как раз об этом: почему в одном случае переменная сбрасывается, а в другом (с тем же setState) — нет. Давайте разберёмся без хайпа, как это работает на практике.

Локальные переменные vs React State

Когда компонент ререндерится, React выполняет функцию компонента заново. Это значит:

function Counter() {
  let count = 0; // Инициализируется при каждом render
  
  const handleClick = () => {
    count = count + 1;
    console.log(count); // В консоли будет +1, но UI не обновится
  };

  return <button onClick={handleClick}>{count}</button>;
}

Здесь count — обычная переменная. При клике она увеличивается, но:

  1. React не знает об изменении
  2. При следующем render переменная снова инициализируется как 0

Почему так? Потому что это просто переменная в scope функции, а не часть реактивного state. React не отслеживает такие изменения.

Управляемые инпуты и сохранение состояния

Теперь смотрим на пример из официальной документации:

function Clock() {
  const [time, setTime] = useState(new Date());
  
  // Эффект для обновления времени
  useEffect(() => {
    const timerID = setInterval(() => {
      setTime(new Date()); // Триггерим rerender
    }, 1000);
    return () => clearInterval(timerID);
  }, []);

  return (
    <div>
      <input />
      <p>{time.toLocaleTimeString()}</p>
    </div>
  );
}

Почему значение в <input> не сбрасывается, хотя компонент ререндерится каждую секунду? Всё дело в том, как работает reconciliation:

  1. React сохраняет состояние инпута между renders, если это управляемый компонент (без явного указания value) или uncontrolled компонент с defaultValue
  2. При rerender React сравнивает виртуальный DOM и обновляет только то, что изменилось (в данном случае — только time в параграфе)

Где ловушки?

  1. Кастомные компоненты без ключей:

    {items.map(item => <ChildComponent data={item} />)} 
    

    При rerender React может пересоздать компоненты, если забыли про key

  2. Inline-функции в пропсах:

    <Button onClick={() => doSomething()} />
    

    Создаётся новая функция при каждом render, что может привести к неожиданному ререндеру дочерних компонентов

Что делать на практике?

  1. Для данных, которые должны сохраняться между renders — только useState, useRef или внешние стейт-менеджеры
  2. Для вычислений, которые не должны запускаться при каждом render — useMemo
  3. Для колбэков, которые передаются вниз по дереву — useCallback

Важный нюанс: useRef сохраняет значение между renders, но не триггерит обновление. Пример счётчика с правильной реализацией:

function Counter() {
  const countRef = useRef(0);
  const [_, forceUpdate] = useState(); // Для триггера rerender
  
  const handleClick = () => {
    countRef.current += 1;
    forceUpdate({}); // Явно запрашиваем rerender
  };

  return <button onClick={handleClick}>{countRef.current}</button>;
}

Какой вывод? React не магия — это модель, где явное лучше неявного. Если нужно сохранять данные между renders, всегда используйте хуки состояния или рефы. Локальные переменные в функции компонента — это всего лишь локальные переменные, которые React даже не пытается отслеживать.


Источник: https://www.reddit.com/r/reactjs/comments/1upd3mk/need_help_understanding_state/