Реактивна незмінність, чому? І як?

В 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.

pic

У цих структурах дані з простими типами та з типами за посиланням зберігаються по-різному.

Прості типи зберігаються в call stack, тоді як типи за посиланням зберігаються в memory heap, і в call stack зберігається лише посилання на пам'ять.

pic

Прості типи змінити не можна. Це не означає, що нові значення не можна присвоювати змінним, а лише те, що значення в області call stack не змінюється.

Коли ми присвоюємо нове значення змінній простого типу, це фактично додає нову адресу в пам'яті, і нова змінна починає посилатися на цю адресу. Старе значення залишається незмінним у пам'яті, а нове значення присвоюється новій області пам'яті. (Стара адреса автоматично очищається за допомогою збирача сміття, оскільки більше на неї немає посилань.)

pic

У випадку з типами за посиланням при переназначенні значення змінюється лише дані в memory heap, а посилання в call stack залишаються незмінними.

Отже, зміна значень типів за посиланням порушує принцип незмінності в пам'яті.

pic

❓ Чому в 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, для підвищення ефективності, порівнюються не значення масивів чи об'єктів, а їхні посилання, тому для оновлення стану необхідно дотримуватися принципу незмінності.
  • Щоб оновити стан об'єкта чи масиву без порушення незмінності, треба створювати новий масив чи об'єкт з новим посиланням.

Перекладено з: 리액트의 불변성, 왜? 그리고 어떻게?

Leave a Reply

Your email address will not be published. Required fields are marked *