Незмінність у стані React
Чому незмінність важлива в React?
React покладається на порівняння посилань для виявлення змін стану. Якщо ви безпосередньо змінюєте об'єкт, React не виявить зміну і не перерисує. Ось чому стан завжди повинен оновлюватися незмінно — створюючи новий об'єкт/масив замість модифікації існуючого.
Проблема: Пряма мутація
tsx
const [user, setUser] = useState({ name: "Alice", age: 25 });
// ❌ НЕПРАВИЛЬНО — мутація існуючого об'єкта
function updateAge() {
user.age = 26; // Те саме посилання!
setUser(user); // React бачить те саме посилання → немає перерисовки
}
// ❌ НЕПРАВИЛЬНО — мутація існуючого масиву
const [items, setItems] = useState(["a", "b", "c"]);
function addItem() {
items.push("d"); // Мутує оригінальний масив
setItems(items); // Те саме посилання → немає перерисовки
}Рішення: Незмінні оновлення
Об'єкти
tsx
const [user, setUser] = useState({ name: "Alice", age: 25 });
// ✅ Оператор розподілу — створює новий об'єкт
setUser({ ...user, age: 26 });
// ✅ Вкладені об'єкти
const [state, setState] = useState({
user: { name: "Alice", address: { city: "Kyiv" } }
});
setState({
...state,
user: {
...state.user,
address: {
...state.user.address,
city: "Lviv"
}
}
});Масиви
tsx
const [items, setItems] = useState([1, 2, 3]);
// ✅ Додати елемент
setItems([...items, 4]);
// ✅ Видалити елемент
setItems(items.filter(item => item !== 2));
// ✅ Оновити елемент
setItems(items.map(item => item === 2 ? 20 : item));
// ✅ Вставити за індексом
const index = 1;
setItems([...items.slice(0, index), 99, ...items.slice(index)]);Чек-лист незмінних операцій
| Операція | Мутуюча (❌) | Незмінна (✅) |
|---|---|---|
| Додати до масиву | push(), unshift() | [...arr, item], [item, ...arr] |
| Видалити з масиву | splice(), pop() | filter() |
| Замінити в масиві | arr[i] = x | map() |
| Сортувати масив | sort() | [...arr].sort() |
| Оновити об'єкт | obj.key = val | { ...obj, key: val } |
| Видалити властивість | delete obj.key | const { key, ...rest } = obj |
Чому React потребує незмінності
tsx
// React порівнює посилання (Object.is)
const prevState = { name: "Alice" };
const nextState = prevState;
prevState === nextState // true → React пропускає перерисовку
// Новий об'єкт = нове посилання
const nextState = { ...prevState, name: "Bob" };
prevState === nextState // false → React перерисовує ✅Це також пояснює, чому React.memo, useMemo та useCallback працюють — вони порівнюють посилання.
Бібліотеки для складних незмінних оновлень
Immer (найпопулярніша)
tsx
import { produce } from "immer";
const [state, setState] = useState({
users: [
{ id: 1, name: "Alice", scores: [90, 85] },
{ id: 2, name: "Bob", scores: [70, 75] },
]
});
// ✅ Immer дозволяє вам "мутувати" чернетку — створює незмінний результат
setState(produce(draft => {
const user = draft.users.find(u => u.id === 1);
if (user) {
user.name = "Alice Updated";
user.scores.push(95);
}
}));useImmerReducer
tsx
import { useImmerReducer } from "use-immer";
const [state, dispatch] = useImmerReducer(
(draft, action) => {
switch (action.type) {
case "addTodo":
draft.todos.push(action.payload); // "мутація" безпечна з Immer
break;
case "toggleTodo":
const todo = draft.todos.find(t => t.id === action.id);
if (todo) todo.done = !todo.done;
break;
}
},
{ todos: [] }
);Важливо:
Ніколи не мутуйте стан React безпосередньо. Завжди створюйте нові об'єкти/масиви, щоб викликати перерисовки. Використовуйте оператор розподілу для простих оновлень і Immer для складного вкладеного стану. Незмінність є основою того, як React виявляє зміни, оптимізує рендеринг і робить стан передбачуваним.
Коротка відповідь
Для співбесідиPremium
Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.