Пошук уроків, статей та іншого контенту
З’ясуємо, що таке Immutability, чому не можна мутувати State напряму та як це впливає на ререндеринг.
Immutability — це принцип, за яким дані після створення не змінюються напряму. Якщо потрібно отримати нове значення, створюють новий об’єкт або масив на основі попереднього.
У React цей принцип особливо важливий для:
state;
props;
об’єктів і масивів, які зберігаються у state;
значень, що передаються дочірнім компонентам.
Наприклад, замість зміни властивості наявного об’єкта:
user.name = "Олена";створюють новий об’єкт:
const updatedUser = {
...user,
name: "Олена",
};Початковий об’єкт user не змінюється, а updatedUser отримує нове посилання в пам’яті.
Стан компонента потрібно оновлювати через функцію, яку повертає useState:
const [count, setCount] = useState(0);Функція setCount повідомляє React, що стан потрібно оновити та, найімовірніше, повторно відрендерити компонент.
Пряма зміна змінної стану не повідомляє React про оновлення:
count = count + 1;Такий код є неправильним, тому що:
React не отримує сигналу про зміну;
компонент може не перерендеритися;
інші частини застосунку можуть продовжити працювати зі старим значенням;
поведінка компонента стає непередбачуваною.
Правильний варіант:
setCount(count + 1);А якщо нове значення залежить від попереднього, краще використати функціональне оновлення:
setCount((previousCount) => previousCount + 1);Функціональне оновлення особливо важливе, коли кілька оновлень стану виконуються послідовно або асинхронно.
Примітивні значення, наприклад числа, рядки та булеві значення, порівнюються за значенням:
1 === 1; // true
"React" === "React"; // trueОб’єкти та масиви порівнюються за посиланням:
const firstUser = { name: "Олена" };
const secondUser = { name: "Олена" };
console.log(firstUser === secondUser); // falseХоча об’єкти мають однаковий вміст, це два різні об’єкти в пам’яті.
Якщо створити новий об’єкт на основі старого, посилання буде новим:
const user = { name: "Олена", age: 25 };
const updatedUser = {
...user,
age: 26,
};
console.log(user === updatedUser); // falseСаме створення нового посилання дозволяє React виявити, що значення змінилося.
React використовує порівняння значень і посилань, щоб визначити, чи потрібно оновлювати компонент.
Розглянемо неправильне оновлення об’єкта:
const user = {
name: "Олена",
age: 25,
};
user.age = 26;
setUser(user);У цьому випадку об’єкт user залишився тим самим об’єктом. Його властивість змінилася, але посилання не змінилося.
Правильне оновлення створює новий об’єкт:
setUser((previousUser) => ({
...previousUser,
age: 26,
}));Тепер React отримує нове посилання і може коректно обробити оновлення.
Важливо: React не виконує глибоке порівняння всіх властивостей об’єкта під час кожного оновлення. Тому потрібно самостійно створювати нові об’єкти на потрібному рівні вкладеності.
Розглянемо компонент із формою користувача:
import { useState } from "react";
export default function UserProfile() {
const [user, setUser] = useState({
name: "Олена",
age: 25,
});
function increaseAge() {
setUser((previousUser) => ({
...previousUser,
age: previousUser.age + 1,
}));
}
function changeName(event) {
const newName = event.target.value;
setUser((previousUser) => ({
...previousUser,
name: newName,
}));
}
return (
<section>
<h2>{user.name}</h2>
<p>Вік: {user.age}</p>
<label>
Ім’я:
<input value={user.name} onChange={changeName} />
</label>
<button onClick={increaseAge}>Збільшити вік</button>
</section>
);
}У кожному оновленні:
previousUser залишається незмінним;
оператор розгортання ...previousUser копіює наявні властивості;
змінена властивість отримує нове значення;
створюється новий об’єкт для state.
Масиви також не можна змінювати напряму.
Неправильно:
items.push(newItem);
setItems(items);Метод push() змінює наявний масив.
Правильно:
setItems((previousItems) => [
...previousItems,
newItem,
]);Для видалення можна використати filter(). Цей метод повертає новий масив:
setItems((previousItems) =>
previousItems.filter((item) => item.id !== itemId)
);Для оновлення одного елемента можна використати map():
setItems((previousItems) =>
previousItems.map((item) =>
item.id === itemId
? { ...item, completed: !item.completed }
: item
)
);Цей код:
створює новий масив;
створює новий об’єкт лише для зміненого елемента;
залишає інші елементи без змін.
import { useState } from "react";
const initialTasks = [
{ id: 1, title: "Вивчити State", completed: true },
{ id: 2, title: "Повторити Immutability", completed: false },
];
export default function TaskList() {
const [tasks, setTasks] = useState(initialTasks);
const [title, setTitle] = useState("");
function addTask(event) {
event.preventDefault();
const trimmedTitle = title.trim();
if (!trimmedTitle) {
return;
}
setTasks((previousTasks) => [
...previousTasks,
{
id: Date.now(),
title: trimmedTitle,
completed: false,
},
]);
setTitle("");
}
function toggleTask(taskId) {
setTasks((previousTasks) =>
previousTasks.map((task) =>
task.id === taskId
? { ...task, completed: !task.completed }
: task
)
);
}
function removeTask(taskId) {
setTasks((previousTasks) =>
previousTasks.filter((task) => task.id !== taskId)
);
}
return (
<section>
<h2>Список завдань</h2>
<form onSubmit={addTask}>
<input
value={title}
onChange={(event) => setTitle(event.target.value)}
placeholder="Нове завдання"
/>
<button type="submit">Додати</button>
</form>
<ul>
{tasks.map((task) => (
<li key={task.id}>
<label>
<input
type="checkbox"
checked={task.completed}
onChange={() => toggleTask(task.id)}
/>
{task.title}
</label>
<button type="button" onClick={() => removeTask(task.id)}>
Видалити
</button>
</li>
))}
</ul>
</section>
);
}У цьому прикладі:
setTasks використовується для кожного оновлення;
map() створює новий масив під час зміни завдання;
filter() створює новий масив під час видалення;
оператор ...task створює новий об’єкт зміненого завдання;
початковий масив і його елементи не мутуються.
Часткова копія об’єкта не копіює автоматично всі вкладені об’єкти.
Наприклад:
const user = {
name: "Олена",
address: {
city: "Львів",
country: "Україна",
},
};Неправильне оновлення:
setUser((previousUser) => {
previousUser.address.city = "Київ";
return {
...previousUser,
};
});Зовнішній об’єкт буде новим, але address залишиться тим самим зміненим об’єктом.
Правильне оновлення потребує створення копії на кожному рівні, який змінюється:
setUser((previousUser) => ({
...previousUser,
address: {
...previousUser.address,
city: "Київ",
},
}));Тут:
створюється новий об’єкт користувача;
створюється новий об’єкт address;
інші властивості залишаються без змін.
Коли стан оновлюється незмінним способом, React може порівняти старе та нове посилання.
Наприклад:
const previousItems = [{ id: 1, active: false }];
const nextItems = previousItems.map((item) =>
item.id === 1
? { ...item, active: true }
: item
);
console.log(previousItems === nextItems); // false
console.log(previousItems[0] === nextItems[0]); // falseМасив і змінений елемент мають нові посилання.
Для елемента, який не змінювався, React може використати старе посилання:
const previousItems = [
{ id: 1, active: false },
{ id: 2, active: false },
];
const nextItems = previousItems.map((item) =>
item.id === 1
? { ...item, active: true }
: item
);
console.log(previousItems[1] === nextItems[1]); // trueЦе допомагає React ефективніше визначати, які частини інтерфейсу потребують оновлення.
Immutability не означає, що перерендеринг завжди відбувається лише через зміну даних. Компонент може перерендеритися через оновлення батьківського компонента, контексту або іншого стану. Проте незмінне оновлення дає React коректні посилання для порівняння та оптимізації.
props надходять до компонента від батьківського компонента. Дочірній компонент не повинен змінювати їх напряму.
Неправильно:
function UserCard({ user }) {
user.name = "Нове ім’я";
return <h2>{user.name}</h2>;
}Замість цього дочірній компонент може повідомити батьківський про подію, а батьківський компонент змінить свій стан:
function UserCard({ user, onNameChange }) {
return (
<label>
Ім’я:
<input
value={user.name}
onChange={(event) => onNameChange(event.target.value)}
/>
</label>
);
}Так відповідальність за зміну даних залишається у компонента, який володіє цим станом.
push, pop, spliceЦі методи змінюють наявний масив:
items.push(newItem);
items.splice(index, 1);Для оновлення стану використовуйте методи, що повертають нові масиви:
додавання — [...];
видалення — filter();
оновлення — map();
сортування — спочатку створіть копію через [...items].
Приклад:
setItems((previousItems) =>
[...previousItems].sort((first, second) =>
first.name.localeCompare(second.name)
)
);Неправильно:
setUser((previousUser) => {
previousUser.age += 1;
return previousUser;
});Правильно:
setUser((previousUser) => ({
...previousUser,
age: previousUser.age + 1,
}));Оператор ... не створює глибоку копію вкладених об’єктів:
const copy = { ...original };Якщо змінюється вкладена властивість, потрібно скопіювати кожен об’єкт на шляху до неї:
setSettings((previousSettings) => ({
...previousSettings,
appearance: {
...previousSettings.appearance,
theme: "dark",
},
}));propsДочірній компонент не повинен змінювати об’єкт або масив, отриманий через props. Для цього потрібно викликати callback, переданий батьківським компонентом.
Під час роботи з об’єктами та масивами корисно перевіряти посилання:
const nextState = {
...previousState,
completed: true,
};
console.log(previousState === nextState); // falseДля масиву:
const nextItems = [...previousItems];
console.log(previousItems === nextItems); // falseЯкщо після оновлення порівняння повертає true, можливо, стан був змінений напряму або setter отримав те саме посилання.
Immutability означає, що наявні дані не змінюються напряму.
state потрібно оновлювати через setter із useState.
Об’єкти та масиви порівнюються за посиланням.
Для оновлення об’єкта створюйте новий об’єкт через оператор ....
Для масивів використовуйте map(), filter() та оператор розгортання замість методів, які мутують масив.
Для вкладених даних створюйте копію на кожному зміненому рівні.
Не мутуйте props.
Нові посилання допомагають React коректно визначати зміни та виконувати ререндеринг.