Урок по треку React 7 мин

useState и useReducer без сюрпризов

Снимок состояния, батчинг, функциональные обновления и редьюсеры.

State это снимок

useState возвращает текущее значение и функцию обновления. Важно: значение внутри рендера не меняется. setState не переписывает переменную, а просит React отрендерить компонент заново с новым значением.

const [count, setCount] = useState(0);
function handle() {
  setCount(count + 1);
  console.log(count); // всё ещё 0: это снимок текущего рендера
}

Батчинг и функциональное обновление

Все обновления внутри одного события (и в React 18+ также в промисах, таймаутах, нативных обработчиках) объединяются в один рендер.

setCount(count + 1);
setCount(count + 1);
setCount(count + 1); // итог: 1, все три раза count = 0

Если новое значение зависит от предыдущего, передавайте функцию. React применит её к самому свежему значению в очереди:

setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1); // итог: 3

Если нужно синхронно применить обновление к DOM (редкий случай, например перед замером), есть flushSync из react-dom.

Ленивая инициализация

Аргумент useState используется только при первом рендере, но выражение вычисляется каждый раз. Для дорогой инициализации передайте функцию:

const [todos] = useState(() => loadFromStorage()); // вызовется один раз
const [todos2] = useState(loadFromStorage());      // вызывается каждый рендер

Неизменяемость

React сравнивает старое и новое значение через Object.is. Мутация объекта или массива сохраняет ссылку, и обновления не будет. Всегда создавайте новое значение:

setItems([...items, newItem]);
setItems(items.filter(i => i.id !== id));
setUser({ ...user, address: { ...user.address, city } });

В ES2023 появились неизменяющие методы массивов: toSorted, toReversed, toSpliced, with. С ними удобно не мутировать state.

Что хранить в state

  • Минимум: то, что нельзя вычислить. Полное имя из firstName и lastName вычисляйте в рендере.
  • Не дублируйте props в state без нужды: копия «застынет» на первом значении.
  • Храните id выбранного элемента, а не копию объекта, иначе при обновлении списка копия устареет.
  • Связанные значения, которые меняются вместе, держите в одном объекте или в редьюсере.

useReducer

Когда логика обновления разрастается, её удобно вынести в чистую функцию:

function reducer(state, action) {
  switch (action.type) {
    case 'added':
      return [...state, { id: action.id, text: action.text, done: false }];
    case 'toggled':
      return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t);
    default:
      throw new Error('Неизвестное действие: ' + action.type);
  }
}
const [todos, dispatch] = useReducer(reducer, []);
dispatch({ type: 'added', id: 1, text: 'Купить хлеб' });

Плюсы: вся логика переходов в одном месте, её легко тестировать без React, dispatch стабилен между рендерами. Редьюсер должен быть чистым и не мутировать state.

Частые ошибки

  • Ждут новое значение сразу после setState.
  • Пишут setCount(count + 1) в цикле или таймере и теряют обновления.
  • Мутируют массив (push, sort) и передают ту же ссылку.
  • Вызывают setState прямо в теле компонента без условия: бесконечный цикл рендеров.

Что спросят на собесе

  • Почему после setState переменная не меняется сразу.
  • Что такое батчинг и когда нужен функциональный апдейтер.
  • Почему state нельзя мутировать.
  • Когда выбрать useReducer вместо useState.
  • Что такое ленивая инициализация.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.