Иммутабельность состояния

Разбираем главную ловушку 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 => ...).
Проверьте себя
1. Почему items.push('C'); setItems(items) не перерисует компонент?
Apush не существует в React
BСсылка на массив осталась прежней, и React считает, что состояние не изменилось
CsetItems нельзя вызывать с массивом
Dpush работает только с объектами
2. Как иммутабельно добавить элемент в массив-состояние?
AsetItems(items.push(x))
BsetItems([...items, x])
Citems[items.length] = x
DsetItems(items += x)
3. Когда стоит передавать в сеттер функцию вида setCount(prev => prev + 1)?
AНикогда, это устаревший приём
BКогда новое значение зависит от предыдущего
CТолько для строк
DТолько при первой отрисовке
Поддержать проект