Menu

Aktualizacja tablic i obiektów w stanie React bez mutacji

Żeby zmienić tablicę albo obiekt w stanie Reacta, zbuduj nowe i przekaż je do settera. Dowiedz się, jak dodawać, usuwać, zastępować, wstawiać i sortować elementy, aktualizować zagnieżdżone obiekty i dlaczego mutowanie stanu zostawia ekran bez zmian.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

W React aktualizujesz tablicę albo obiekt w stanie, tworząc nowe i przekazując je do settera. Nigdy nie zmieniaj istniejącej wartości w miejscu: użyj spreadu (...), map, filter albo slice, żeby zbudować kopię z twoją zmianą, a potem wywołaj setItems(copy).

[...items, { ... }] to nowa tablica z jednym elementem więcej, a filter zwraca nową tablicę bez usuniętego elementu. Oba zostawiają starą tablicę w spokoju.

Dlaczego mutacja nie renderuje

Gdy wywołujesz setter, React porównuje nową wartość ze starą przez Object.is. Dla tablic i obiektów porównuje to referencje, a nie zawartość. Jeśli zmienisz tablicę przez push i przekażesz z powrotem tę samą tablicę, React widzi tę samą referencję, uznaje, że nic się nie zmieniło, i pomija renderowanie.

Kliknij kilka razy "push". Konsola pokazuje, że tablica rośnie, ale nie pojawia się żadna nowa linia renderowania, a tekst zostaje a. Potem kliknij "spread": następuje jedno renderowanie, a wszystkie dodane elementy pojawiają się naraz, bo przez cały czas były w tablicy. Mutacja nie rzuca błędu, tylko zostawia ekran nieaktualny, dopóki coś innego nie wywoła renderowania. Kliknij potem "push" jeszcze raz, a tym razem może się wyrenderować: React nie zawsze może pominąć pracę z góry, więc mutacja czasem się pokazuje, a czasem nie. Ta nieprzewidywalność to powód, żeby nie mutować stanu.

Tablice: metody do używania i do unikania

Zasada jest prosta: używaj metod, które zwracają nową tablicę, unikaj metod, które zmieniają tablicę, na której je wywołujesz.

CelUnikaj (mutuje)Używaj (zwraca nową tablicę)
Dodaniepush, unshift[...arr, item], [item, ...arr]
Usunięciepop, shift, splicefilter, slice
Zastąpieniearr[i] = x, splicemap, with(i, x)
Wstawieniesplice[...arr.slice(0, i), x, ...arr.slice(i)], toSpliced
Sortowanie, odwróceniesort, reversetoSorted, toReversed albo najpierw kopia

toSorted, toReversed, toSpliced i with to nowsze metody tablic (ES2023), obsługiwane we wszystkich aktualnych głównych przeglądarkach od 2023 roku. Robią to samo co ich mutujący kuzyni, ale na kopii.

Aktualizacja jednego elementu przez map

Żeby zmienić jeden element, przejdź po tablicy przez map i zwróć nowy obiekt dla elementu, który chcesz zmienić, a dla każdego innego oryginalny obiekt.

Część { ...todo, done: !todo.done } ma znaczenie. Częsty błąd to skopiowanie tablicy, ale zmiana obiektu w jej środku:

const next = [...todos];
next[0].done = true; // next is new, but next[0] is the same object as todos[0]
setTodos(next);

To się renderuje, więc wygląda w porządku, ale stara i nowa tablica dzielą teraz zmieniony obiekt. Wszystko, co porównuje stary element z nowym (dziecko z React.memo, efekt zależny od elementu), nie widzi różnicy. Kopiuj element, który zmieniasz.

Wstawianie pod indeksem

Nie ma niemutującego insert, więc przetnij tablicę na dwie części i umieść nowy element między połówkami. toSpliced(index, 0, item) robi to samo w jednym wywołaniu.

Ustaw indeks na 0 i wstaw ponownie: nowe C trafia na początek. Zastąp wyrażenie next przez letters.toSpliced(index, 0, 'C'), a wynik będzie ten sam.

Sortowanie i odwracanie

sort i reverse zmieniają tablicę, na której są wywołane, choć też ją zwracają. Przez to setItems(items.sort()) to mutacja, po której następuje ustawienie z tą samą referencją, czyli błąd z początku tej strony. Zamiast tego sortuj kopię.

[...scores].reverse() działa, bo spread najpierw robi kopię, a reverse zmienia potem tylko tę kopię.

Aktualizacja obiektów i zagnieżdżonych obiektów

Obiekty podlegają tej samej zasadzie. Rozpakuj stary obiekt do nowego i nadpisz pola, które się zmieniają. Przy zagnieżdżonym obiekcie użyj spreadu na każdym poziomie w drodze do pola: pojedynczy spread kopiuje tylko najwyższy poziom, więc wewnętrzny obiekt nadal byłby stary.

Każdy kopiowany poziom to nowy obiekt; każdy poziom, którego nie dotykasz, zostaje tym samym obiektem. To sprawia, że spread jest tani: gdyby stan zawierał też długą listę zamówień, { ...user } skopiowałoby referencję do listy, a nie samą listę.

Jeśli obiekt w stanie jest zagnieżdżony na trzy albo cztery poziomy i ciągle piszesz długie łańcuchy spreadów, często oznacza to, że stan mógłby być bardziej płaski. Przechowywanie elementów w słowniku po id albo podział jednego dużego obiektu na kilka wywołań useState sprawia, że każda aktualizacja jest krótka. Gdy wiele zdarzeń zmienia jedną strukturę, pomaga też reduktor (zobacz useReducer).

structuredClone a spread

structuredClone(user) robi głęboką kopię w jednym wywołaniu, więc potem możesz swobodnie zmieniać kopię:

const next = structuredClone(user);
next.address.city = 'Oslo';
setUser(next);

To jest poprawne, ale za każdym razem kopiuje cały obiekt, łącznie z częściami, których nie zmieniłeś. Kosztuje to czas przy dużym stanie i oznacza, że każdy zagnieżdżony obiekt dostaje nową referencję, więc zmemoizowane dziecko, które dostaje user.orders, renderuje się ponownie, choć zamówienia się nie zmieniły. structuredClone rzuca też błąd na funkcjach, a instancje klas wracają jako zwykłe obiekty. Spread kopiuje tylko ścieżkę, którą zmieniasz, więc to zwykle on jest wybierany; structuredClone zostaw na rzadką aktualizację, która naprawdę przepisuje większość obiektu.

Pisanie mutacji z Immer

Immer to mała biblioteka, która pozwala pisać kod wyglądający jak mutacja. Daje ci szkic (draft), zapisuje, co zmieniasz, i tworzy nową niemutowalną wartość, kopiując tylko zmienione części. Pakiet use-immer opakowuje ją w hook:

import { useImmer } from 'use-immer';

function Profile() {
    const [user, updateUser] = useImmer({
        name: 'Ada',
        address: { city: 'London' },
    });

    function moveTo(city) {
        updateUser((draft) => {
            draft.address.city = city;
        });
    }
    // ...
}

Immer nie jest dostępny w edytorach na tej stronie, które ładują tylko React. Warto go dodać, gdy stan jest głęboko zagnieżdżony, a spready stają się trudne do czytania. Przy płaskim stanie wystarczą zwykły spread i map i to je zobaczysz w większości kodu w React.

Najczęściej zadawane pytania

Jak dodać element do tablicy w stanie Reacta?

Utwórz nową tablicę, która zawiera stare elementy i nowy, i przekaż ją do settera: setItems([...items, newItem]). Nie wywołuj items.push(newItem), bo to zmienia istniejącą tablicę, a React widzi tę samą referencję.

Dlaczego mój komponent nie renderuje się ponownie, gdy aktualizuję tablicę?

Najpewniej zmieniłeś tablicę w miejscu (push, splice, sort albo arr[i] = x) i przekazałeś z powrotem tę samą tablicę. React porównuje starą i nową wartość przez Object.is, znajduje ten sam obiekt i pomija renderowanie. Przekaż zamiast tego nową tablicę.

Jak w React zaktualizować jeden obiekt w tablicy?

Użyj map i zwróć nowy obiekt dla zmienianego elementu, a dla każdego innego oryginalny element: setTodos(todos.map(t => t.id === id ? { ...t, done: !t.done } : t)).

Jak zaktualizować zagnieżdżony obiekt w stanie Reacta?

Skopiuj każdy poziom na ścieżce do zmienianego pola: setUser({ ...user, address: { ...user.address, city: 'Oslo' } }). Pojedynczy spread kopiuje tylko najwyższy poziom, więc wewnętrzny obiekt nadal byłby współdzielony.

Czy dla stanu Reacta używać structuredClone, czy operatora spread?

Wybieraj spread. Kopiuje tylko to, co się zmienia, a nieruszone części zostawia jako te same obiekty, co pomaga React.memo i porównaniom. structuredClone za każdym razem kopiuje wszystko, co łatwiej napisać, ale jest wolniejsze przy dużym stanie i psuje równość referencji dla części, które się nie zmieniły.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ