Пошук уроків, статей та іншого контенту
Навчимося оновлювати вкладені об’єкти у State без прямої зміни їхніх властивостей.
У React значення, які зберігаються у state, потрібно оновлювати незмінно: не змінювати наявний об’єкт, а створювати новий об’єкт із потрібними змінами.
Це особливо важливо для вкладених об’єктів:
const [user, setUser] = useState({
name: "Олена",
settings: {
theme: "light",
notifications: true,
},
});У цьому прикладі user містить вкладений об’єкт settings. Щоб змінити тему, потрібно створити нові об’єкти для всіх рівнів, які змінюються.
Такий код змінює наявний об’єкт безпосередньо:
user.settings.theme = "dark";Це поганий підхід, оскільки:
React може не побачити зміну посилання на об’єкт;
оновлення інтерфейсу може не відбутися очікуваним чином;
початковий стан змінюється без виклику функції-оновлювача;
відстеження змін і налагодження стають складнішими.
Також не варто робити лише поверхневу копію верхнього рівня:
const updatedUser = { ...user };
updatedUser.settings.theme = "dark";У цьому випадку updatedUser.settings і user.settings посилаються на один і той самий вкладений об’єкт. Тому вкладений об’єкт усе одно буде змінено напряму.
Для оновлення вкладеного об’єкта використовують оператор розгортання ... на кожному необхідному рівні:
setUser((currentUser) => ({
...currentUser,
settings: {
...currentUser.settings,
theme: "dark",
},
}));Тут відбувається таке:
Створюється новий об’єкт user.
У нього копіюються всі властивості з поточного стану.
Створюється новий об’єкт settings.
У нього копіюються всі старі налаштування.
Властивість theme отримує нове значення.
Інші властивості залишаються без змін.
Коли нове значення залежить від попереднього стану, краще передавати в setter функцію:
setUser((currentUser) => ({
...currentUser,
name: "Марія",
}));Параметр currentUser містить актуальне значення стану. Це надійніше, ніж використовувати змінну зі зовнішньої області:
setUser({
...user,
name: "Марія",
});Функціональне оновлення особливо корисне, коли кілька оновлень стану можуть виконуватися послідовно або асинхронно.
Нижче компонент із профілем користувача. Він дозволяє змінювати ім’я, тему та налаштування сповіщень.
import { useState } from "react";
export default function UserProfile() {
const [user, setUser] = useState({
name: "Олена",
email: "olena@example.com",
settings: {
theme: "light",
notifications: true,
},
});
function handleNameChange(event) {
const newName = event.target.value;
setUser((currentUser) => ({
...currentUser,
name: newName,
}));
}
function handleThemeChange(event) {
const newTheme = event.target.value;
setUser((currentUser) => ({
...currentUser,
settings: {
...currentUser.settings,
theme: newTheme,
},
}));
}
function handleNotificationsChange(event) {
const areNotificationsEnabled = event.target.checked;
setUser((currentUser) => ({
...currentUser,
settings: {
...currentUser.settings,
notifications: areNotificationsEnabled,
},
}));
}
return (
<section>
<h2>Профіль користувача</h2>
<label>
Ім’я:
<input
type="text"
value={user.name}
onChange={handleNameChange}
/>
</label>
<br />
<label>
Тема:
<select value={user.settings.theme} onChange={handleThemeChange}>
<option value="light">Світла</option>
<option value="dark">Темна</option>
</select>
</label>
<br />
<label>
<input
type="checkbox"
checked={user.settings.notifications}
onChange={handleNotificationsChange}
/>
Отримувати сповіщення
</label>
<pre>{JSON.stringify(user, null, 2)}</pre>
</section>
);
}Для зміни name достатньо скопіювати об’єкт верхнього рівня:
setUser((currentUser) => ({
...currentUser,
name: "Марія",
}));Для зміни settings.theme потрібно скопіювати і user, і settings:
setUser((currentUser) => ({
...currentUser,
settings: {
...currentUser.settings,
theme: "dark",
},
}));За один виклик можна оновити кілька властивостей вкладеного об’єкта:
setUser((currentUser) => ({
...currentUser,
settings: {
...currentUser.settings,
theme: "dark",
notifications: false,
},
}));Старі властивості user і settings, яких немає в оновленні, збережуться завдяки оператору ....
Якщо форма має кілька полів у settings, можна використовувати один обробник:
import { useState } from "react";
export default function SettingsForm() {
const [settings, setSettings] = useState({
theme: "light",
language: "uk",
notifications: true,
});
function handleSettingChange(event) {
const { name, value, type, checked } = event.target;
const nextValue = type === "checkbox" ? checked : value;
setSettings((currentSettings) => ({
...currentSettings,
[name]: nextValue,
}));
}
return (
<form>
<label>
Тема:
<select
name="theme"
value={settings.theme}
onChange={handleSettingChange}
>
<option value="light">Світла</option>
<option value="dark">Темна</option>
</select>
</label>
<br />
<label>
Мова:
<select
name="language"
value={settings.language}
onChange={handleSettingChange}
>
<option value="uk">Українська</option>
<option value="en">English</option>
</select>
</label>
<br />
<label>
<input
name="notifications"
type="checkbox"
checked={settings.notifications}
onChange={handleSettingChange}
/>
Отримувати сповіщення
</label>
<pre>{JSON.stringify(settings, null, 2)}</pre>
</form>
);
}Запис:
[name]: nextValueозначає, що назва властивості береться зі значення змінної name. Наприклад, якщо name дорівнює "theme", результат буде таким:
{
theme: nextValue
}Якщо об’єкт має більше рівнів вкладеності, потрібно створювати копію кожного рівня, який веде до змінюваної властивості:
const [account, setAccount] = useState({
profile: {
contact: {
email: "olena@example.com",
},
},
});Оновлення email:
setAccount((currentAccount) => ({
...currentAccount,
profile: {
...currentAccount.profile,
contact: {
...currentAccount.profile.contact,
email: "new@example.com",
},
},
}));Не потрібно копіювати рівні, які не змінюються. Але кожен об’єкт на шляху до нової властивості має бути скопійований.
Для властивості верхнього рівня копія має такий вигляд:
setUser((currentUser) => ({
...currentUser,
name: "Андрій",
}));Для вкладеної властивості:
setUser((currentUser) => ({
...currentUser,
settings: {
...currentUser.settings,
theme: "dark",
},
}));Оператор ... не змінює старий об’єкт. Він копіює його властивості в новий об’єкт.
user.settings.theme = "dark";Потрібно створити нові об’єкти:
setUser((currentUser) => ({
...currentUser,
settings: {
...currentUser.settings,
theme: "dark",
},
}));setUser((currentUser) => {
const nextUser = { ...currentUser };
nextUser.settings.theme = "dark";
return nextUser;
});settings у цьому прикладі залишається старим об’єктом. Правильний варіант:
setUser((currentUser) => ({
...currentUser,
settings: {
...currentUser.settings,
theme: "dark",
},
}));setUser((currentUser) => ({
...currentUser,
settings: {
theme: "dark",
},
}));У цьому випадку властивість notifications буде видалена з нового settings.
Потрібно спочатку скопіювати старі властивості:
setUser((currentUser) => ({
...currentUser,
settings: {
...currentUser.settings,
theme: "dark",
},
}));setUser({
...user,
name: "Олена",
});Якщо новий стан залежить від попереднього, використовуйте функціональне оновлення:
setUser((currentUser) => ({
...currentUser,
name: "Олена",
}));Для оновлення вкладеної властивості дотримуйтеся такого шаблону:
setState((currentState) => ({
...currentState,
nestedObject: {
...currentState.nestedObject,
property: newValue,
},
}));Для глибшої вкладеності шаблон продовжується:
setState((currentState) => ({
...currentState,
firstLevel: {
...currentState.firstLevel,
secondLevel: {
...currentState.firstLevel.secondLevel,
property: newValue,
},
},
}));Не змінюйте властивості об’єкта у state напряму.
Для оновлення використовуйте setter, наприклад setUser.
Копіюйте об’єкт верхнього рівня за допомогою ....
Для вкладених об’єктів створюйте копію кожного рівня, який змінюється.
Щоб не втратити інші властивості, спочатку розгорніть старий вкладений об’єкт.
Якщо нове значення залежить від попереднього стану, використовуйте функціональне оновлення.