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.
| Cel | Unikaj (mutuje) | Używaj (zwraca nową tablicę) |
|---|---|---|
| Dodanie | push, unshift | [...arr, item], [item, ...arr] |
| Usunięcie | pop, shift, splice | filter, slice |
| Zastąpienie | arr[i] = x, splice | map, with(i, x) |
| Wstawienie | splice | [...arr.slice(0, i), x, ...arr.slice(i)], toSpliced |
| Sortowanie, odwrócenie | sort, reverse | toSorted, 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.