Состояние и хуки — Примеры

Пример 1 — Кнопка, прибавляющая один вместо двух

Обработчик дважды вызывает сеттер с одним и тем же выражением, поэтому счётчик увеличивается на один: скажите, какое значение прочитал каждый вызов, и заставьте оба вызова опираться на ожидающее значение.

function Counter() {
  const [count, setCount] = useState(0);

  function addTwo() {
    setCount(count + 1);
    setCount(count + 1);   // наблюдаемый результат: 1
  }

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

Решение

function Counter() {
  const [count, setCount] = useState(0);

  function addTwo() {
    setCount((c) => c + 1);
    setCount((c) => c + 1);
  }

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

Состояние — это снимок, а не живая переменная: каждый рендер захватывает значения, с которыми был вызван, и оба вызова в этом обработчике замыкаются на один и тот же count из создавшего их рендера, поэтому вычисляют одно и то же число, а побеждает вторая запись. Форма с функцией-обновителем получает ожидающее значение вместо копии из рендера — она и безопасна по умолчанию всегда, когда следующее значение зависит от предыдущего. По той же причине чтение переменной сразу после установки возвращает старое значение: React собирает обновления одного события и рендерит один раз, а не порождает три несогласованных кадра.

Пример 2 — Свойство, однажды скопированное в состояние

Поле суммы берёт стартовое значение из свойства и хранит его. Когда родитель загружает другой заказ, поле продолжает показывать первый. Скажите, почему скопированное в состояние значение перестало обновляться, а затем перестаньте его копировать.

function PriceInput({ initialAmount }) {
  const [amount, setAmount] = useState(initialAmount);   // задано один раз, больше никогда
  return <input value={amount} onChange={(e) => setAmount(Number(e.target.value))} />;
}

Решение

function PriceInput({ amount, onAmountChange }) {
  return <input value={amount} onChange={(e) => onAmountChange(Number(e.target.value))} />;
}

// теперь значение принадлежит родителю
<PriceInput amount={amount} onAmountChange={setAmount} />

Копирование свойства в состояние замораживает первое значение, которое компонент вообще увидел: инициализатор выполняется один раз, а последующие изменения свойства просто игнорируются, поэтому ошибка выглядит как проблема рендеринга, хотя на деле она о владении данными. Исправление — читать свойство напрямую и позволить родителю владеть значением, что заодно делает поле контролируемым и тестируемым. Там, где компоненту действительно нужен собственный черновик — форма, которую правят до сохранения, — ключ по id записи пересоздаёт компонент при её смене, и это намеренно сбрасывает черновик вместо хранения устаревшего. Никогда не храните то, что выводится из свойств.