Иммутабельность состояния
Разбираем главную ловушку useState: почему push и прямое изменение объекта не перерисовывают компонент.
Иммутабельное обновление — создание нового массива или объекта вместо изменения существующего; только так React замечает, что состояние поменялось.
Почему мутация не работает
React решает, нужна ли перерисовка, сравнивая ссылку на старое и новое значение. Если вы изменили массив на месте (push), ссылка осталась прежней — React видит «то же самое» и не перерисовывает.
const [items, setItems] = useState(["A", "B"]);
function addBad() {
items.push("C"); // ❌ мутация: ссылка та же
setItems(items); // React: «массив не изменился» → нет перерисовки
}
function addGood() {
setItems([...items, "C"]); // ✅ новый массив → новая ссылка → перерисовка
}
Сравнение по ссылке на чистом JS
Чтобы прочувствовать, почему ссылка решает всё, посмотрите без React:
const a = ["A", "B"];
const b = a; // та же ссылка
b.push("C");
console.log(a === b); // та же ссылка → true (React: "не менялось")
const c = ["A", "B"];
const d = [...c, "C"]; // новый массив
console.log(c === d); // разные ссылки → false (React: "изменилось!")
console.log(d);
Вывод:
true false [ 'A', 'B', 'C' ]
Вывод: push оставляет ту же ссылку (true), а spread создаёт новую (false). React реагирует именно на смену ссылки.
Шаблоны иммутабельного обновления
Запомните несколько приёмов — они покрывают почти все случаи.
Массив: добавить
setItems([...items, newItem]);
Массив: удалить
setItems(items.filter((it) => it.id !== id));
Массив: изменить элемент
setItems(items.map((it) =>
it.id === id ? { ...it, done: true } : it
));
Объект: обновить поле
setUser({ ...user, age: user.age + 1 });
Общий принцип: filter, map и spread ... возвращают новые структуры, не трогая исходные. А push, splice, прямое присваивание user.age = — мутируют и потому запрещены для состояния.
Функция-обновитель
Когда новое состояние зависит от предыдущего (особенно при нескольких обновлениях подряд), передавайте в сеттер функцию. React передаст в неё актуальное значение:
setCount((prev) => prev + 1); // надёжнее, чем setCount(count + 1)
Памятка
| Мутирует (нельзя) | Иммутабельно (нужно) |
arr.push(x) | [...arr, x] |
arr.splice(...) | arr.filter(...) |
obj.key = v | { ...obj, key: v } |
Итог
- React сравнивает состояние по ссылке — мутация на месте (
push,obj.key =) перерисовку не вызывает. - Обновляйте иммутабельно: spread
...,map,filterсоздают новые структуры. - Если новое значение зависит от старого — используйте функцию-обновитель
setX(prev => ...).