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. - Что такое ленивая инициализация.