В React за допомогою хука useState() визначають стан, а зміни в даних можна здійснювати лише за допомогою повернутого функціонала setter.
У випадку з типами даних, які зберігають не один, а кілька елементів, таких як масиви чи об'єкти, значення слід змінювати наступним чином.
// Приклад додавання нового елемента в масив items
const [items, setItems] = useState(['Item 1', 'Item 2', 'Item3']);
const addItem = () => {
setItems([...items, 'New Item']); // Копіюємо існуючий масив та додаємо новий елемент
};
В JavaScript є методи, такі як push(), для додавання елементів у масив, але чому ми маємо використовувати тільки функцію setter для зміни значень? І чому функція setter не змінює лише конкретне значення, а замість цього перезаписує весь масив чи об'єкт?
Це все через те, що в React необхідно дотримуватись незмінності (immutability).
Незмінність означає, що значення або стан не можуть бути змінені безпосередньо. В JavaScript це означає, що неможливо змінити значення в пам'яті.
Отже, чому в React важливо зберігати незмінність і як її можна забезпечити?
Щоб зрозуміти незмінність в React, спершу треба розібратися з пам'ятю JavaScript та принципом переназначення даних.
JavaScript використовує дві основні структури пам'яті: call stack та memory heap.
У цих структурах дані з простими типами та з типами за посиланням зберігаються по-різному.
Прості типи зберігаються в call stack, тоді як типи за посиланням зберігаються в memory heap, і в call stack зберігається лише посилання на пам'ять.
Прості типи змінити не можна. Це не означає, що нові значення не можна присвоювати змінним, а лише те, що значення в області call stack не змінюється.
Коли ми присвоюємо нове значення змінній простого типу, це фактично додає нову адресу в пам'яті, і нова змінна починає посилатися на цю адресу. Старе значення залишається незмінним у пам'яті, а нове значення присвоюється новій області пам'яті. (Стара адреса автоматично очищається за допомогою збирача сміття, оскільки більше на неї немає посилань.)
У випадку з типами за посиланням при переназначенні значення змінюється лише дані в memory heap, а посилання в call stack залишаються незмінними.
Отже, зміна значень типів за посиланням порушує принцип незмінності в пам'яті.
❓ Чому в React важлива незмінність?
React відстежує зміни в state і викликає повторний рендер компонентів, і це відбувається на основі значень в call stack.
Для підвищення ефективності React здійснює поверхневе порівняння (shallow comparison) при зміні стану. Поверхневе порівняння означає, що React не порівнює зміни всіх властивостей масиву чи об'єкта, а лише порівнює значення посилань (тобто адреси пам'яті, які зберігаються в call stack). У випадку з простими типами порівнюються самі значення.
Коли ми змінюємо значення типу за посиланням, зміни відбуваються в memory heap, але посилання в call stack залишаються незмінними, і тому React не може відстежити зміни. Через це навіть якщо значення змінюється, компонент не буде перерендерено.
❗ Як забезпечити незмінність?
Отже, коли ми змінюємо масиви чи об'єкти, які є типами за посиланням, ми не маємо змінювати самі дані, а маємо створити новий масив або об'єкт з новим посиланням. Тому при зміні стану, що містить типи за посиланням, ми повинні передавати в setter функцію нові значення, не забуваючи про збереження всіх незмінних даних.
Щоб створити новий масив чи об'єкт, ми можемо використовувати оператор розповсюдження (...).
// Оновлення масиву
const [items, setItems] = useState([1, 2, 3]);
setItems((prevItems) => [...prevItems, 4]); // Копіюємо існуючий масив і додаємо новий елемент
// Оновлення об'єкта
const [user, setUser] = useState({
name: "John",
age: 25,
});
setUser((prevUser) => ({
...prevUser, // Копіюємо існуючий об'єкт
name: "Jane", // Оновлюємо лише необхідне поле
}));
Також можна використовувати методи, які повертають нові масиви, такі як map(), filter(), slice(), reduce(), concat().
Immer
Immer — це бібліотека, яка допомагає зберігати незмінність і легко змінювати стан.
Використовуючи Immer, можна значно спростити оновлення стану навіть для складних структур даних.
Immer працює на основі функції produce. Функція produce приймає поточний стан, виконує зміни, а потім створює і повертає новий стан.
У середині produce ми працюємо з об'єктом Proxy під назвою draft, що дозволяє нам змінювати дані, як якщо б ми працювали безпосередньо з оригінальними значеннями, але зберігаючи незмінність.
Однак записані зміни не мають жодного впливу на оригінальні дані, і в кінцевому підсумку створюється новий стан, який враховує всі зміни.
Ось як це використовувати:
Спочатку потрібно встановити бібліотеку:
npm install immer
Основний спосіб використання — produce приймає дані, які потрібно змінити, та функцію для змінення стану як параметри.
import {produce} from "immer"
const baseState = [
{
title: "Learn TypeScript",
done: true
},
{
title: "Try Immer",
done: false
}
]
const nextState = produce(baseState, draftState => {
draftState.push({title: "Tweet about it"})
draftState[1].done = true
})
Якщо використовувати з useState(), то можна застосувати це всередині функції setter.
import { useState } from "react";
import produce from "immer";
function App() {
const [state, setState] = useState({
user: { name: "John", age: 25 },
});
const updateAge = () => {
setState(
produce((draft) => {
draft.user.age += 1;
})
);
};
[Підсумок]
- Коли ми змінюємо значення простих типів в JavaScript, нові значення записуються в нову область пам'яті, зберігаючи незмінність. Однак для типів за посиланням зміни відбуваються безпосередньо в пам'яті, що порушує незмінність.
- В React, для підвищення ефективності, порівнюються не значення масивів чи об'єктів, а їхні посилання, тому для оновлення стану необхідно дотримуватися принципу незмінності.
- Щоб оновити стан об'єкта чи масиву без порушення незмінності, треба створювати новий масив чи об'єкт з новим посиланням.
Перекладено з: 리액트의 불변성, 왜? 그리고 어떻게?