In React aktualisierst du ein Array oder ein Objekt im State, indem du ein neues erzeugst und es an den Setter übergibst. Ändere niemals den bestehenden Wert direkt: Nutze Spread (...), map, filter oder slice, um eine Kopie mit deiner Änderung zu bauen, und ruf dann setItems(copy) auf.
[...items, { ... }] ist ein neues Array mit einem Eintrag mehr, und filter gibt ein neues Array ohne den entfernten zurück. Beide lassen das alte Array in Ruhe.
Warum Mutation nicht rendert
Wenn du einen Setter aufrufst, vergleicht React den neuen Wert mit dem alten per Object.is. Bei Arrays und Objekten vergleicht das Referenzen, nicht Inhalte. Wenn du ein Array mit push änderst und dasselbe Array zurückgibst, sieht React dieselbe Referenz, entscheidet, dass sich nichts geändert hat, und überspringt den Render.
Klicke ein paar Mal auf „push“. Die Konsole zeigt, wie das Array wächst, aber keine neue Render-Zeile erscheint, und der Text bleibt a. Klicke dann auf „spread“: Ein Render passiert, und jeder gepushte Eintrag erscheint auf einmal, weil er die ganze Zeit im Array war. Mutation wirft keinen Fehler, sie lässt den Bildschirm nur veraltet, bis etwas anderes einen Render auslöst. Klicke danach noch einmal auf „push“, und diesmal rendert es vielleicht: React kann die Arbeit nicht immer im Voraus überspringen, also zeigt sich eine Mutation manchmal und manchmal nicht. Genau diese Unvorhersehbarkeit ist der Grund, State nicht zu mutieren.
Arrays: Methoden, die du nutzen und meiden solltest
Die Regel ist einfach: Nutze Methoden, die ein neues Array zurückgeben, und meide Methoden, die das Array ändern, auf dem sie aufgerufen werden.
| Ziel | Meiden (mutiert) | Nutzen (gibt ein neues Array zurück) |
|---|---|---|
| Hinzufügen | push, unshift | [...arr, item], [item, ...arr] |
| Entfernen | pop, shift, splice | filter, slice |
| Ersetzen | arr[i] = x, splice | map, with(i, x) |
| Einfügen | splice | [...arr.slice(0, i), x, ...arr.slice(i)], toSpliced |
| Sortieren, umkehren | sort, reverse | toSorted, toReversed oder erst kopieren |
toSorted, toReversed, toSpliced und with sind neuere Array-Methoden (ES2023), die seit 2023 alle aktuellen großen Browser unterstützen. Sie erledigen dieselbe Aufgabe wie ihre mutierenden Verwandten, nur auf einer Kopie.
Einen Eintrag mit map aktualisieren
Um einen Eintrag zu ändern, mappe über das Array und gib für den gewünschten Eintrag ein neues Objekt zurück und für jeden anderen das ursprüngliche Objekt.
Der Teil { ...todo, done: !todo.done } ist wichtig. Ein häufiger Bug ist, das Array zu kopieren, aber das Objekt darin zu ändern:
const next = [...todos];
next[0].done = true; // next is new, but next[0] is the same object as todos[0]
setTodos(next);
Das rendert, sieht also gut aus, aber das alte und das neue Array teilen sich jetzt ein geändertes Objekt. Alles, was den alten Eintrag mit dem neuen vergleicht (ein Kind mit React.memo, ein Effekt, der vom Eintrag abhängt), sieht keinen Unterschied. Kopiere den Eintrag, den du änderst.
An einem Index einfügen
Es gibt kein nicht mutierendes insert, also schneide das Array in zwei Teile und setze den neuen Eintrag zwischen die Hälften. toSpliced(index, 0, item) macht dasselbe in einem Aufruf.
Setze den Index auf 0 und füge erneut ein: Das neue C kommt nach vorn. Ersetze den Ausdruck next durch letters.toSpliced(index, 0, 'C'), und das Ergebnis ist dasselbe.
Sortieren und umkehren
sort und reverse ändern das Array, auf dem sie aufgerufen werden, obwohl sie es auch zurückgeben. Damit ist setItems(items.sort()) eine Mutation, gefolgt von einem Set mit derselben Referenz, also genau der Bug vom Anfang dieser Seite. Sortiere stattdessen eine Kopie.
[...scores].reverse() funktioniert, weil der Spread zuerst die Kopie erzeugt und reverse dann nur diese Kopie ändert.
Objekte und verschachtelte Objekte aktualisieren
Für Objekte gilt dieselbe Regel. Spreade das alte Objekt in ein neues und überschreibe die Felder, die sich ändern. Bei einem verschachtelten Objekt spreadest du auf jeder Ebene auf dem Weg nach unten zum Feld: Ein einzelner Spread kopiert nur die oberste Ebene, also wäre das innere Objekt weiterhin das alte.
Jede Ebene, die du kopierst, ist ein neues Objekt; jede Ebene, die du nicht anfasst, bleibt dasselbe Objekt. Das macht Spread günstig: Wenn der State auch eine lange Liste von Bestellungen hielte, würde { ...user } die Referenz auf die Liste kopieren, nicht die Liste selbst.
Wenn ein Objekt im State drei oder vier Ebenen tief verschachtelt ist und du ständig lange Spread-Ketten schreibst, ist das oft ein Zeichen, dass der State flacher sein könnte. Einträge in einer Zuordnung nach ID zu speichern oder ein großes Objekt auf ein paar useState-Aufrufe aufzuteilen hält jedes Update kurz. Wenn viele Events eine Struktur ändern, hilft auch ein Reducer (siehe useReducer).
structuredClone vs. Spread
structuredClone(user) erstellt in einem Aufruf eine tiefe Kopie, die du dann frei ändern kannst:
const next = structuredClone(user);
next.address.city = 'Oslo';
setUser(next);
Das ist korrekt, kopiert aber jedes Mal das ganze Objekt, auch die Teile, die du nicht geändert hast. Das kostet bei großem State Zeit und bedeutet, dass jedes verschachtelte Objekt eine neue Referenz bekommt, also rendert ein memoisiertes Kind, das user.orders erhält, erneut, obwohl sich die Bestellungen nicht geändert haben. structuredClone wirft außerdem bei Funktionen einen Fehler, und Klasseninstanzen kommen als einfache Objekte zurück. Spread kopiert nur den Pfad, den du änderst, und ist deshalb die übliche Wahl; behalte structuredClone für das seltene Update, das wirklich den Großteil des Objekts neu schreibt.
Mutationen mit Immer schreiben
Immer ist eine kleine Bibliothek, mit der du Code schreibst, der wie Mutation aussieht. Sie gibt dir einen Entwurf, zeichnet auf, was du änderst, und erzeugt einen neuen unveränderlichen Wert, bei dem nur die geänderten Teile kopiert sind. Das Paket use-immer verpackt das in einen 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;
});
}
// ...
}
In den Editoren auf dieser Seite, die nur React laden, ist Immer nicht verfügbar. Es lohnt sich, wenn dein State tief verschachtelt ist und die Spreads schwer lesbar werden. Für flachen State reichen einfacher Spread und map, und genau das siehst du in den meisten React-Codebasen.
Häufig gestellte Fragen
Wie füge ich einem Array im React-State einen Eintrag hinzu?
Erstelle ein neues Array mit den alten Einträgen plus dem neuen und übergib es an den Setter: setItems([...items, newItem]). Ruf nicht items.push(newItem) auf, denn das ändert das bestehende Array, und React sieht dieselbe Referenz.
Warum rendert meine Komponente nicht erneut, wenn ich ein Array aktualisiere?
Höchstwahrscheinlich hast du das Array direkt geändert (push, splice, sort oder arr[i] = x) und dasselbe Array zurückgegeben. React vergleicht alten und neuen Wert mit Object.is, findet dasselbe Objekt und überspringt den Render. Übergib stattdessen ein neues Array.
Wie aktualisiere ich ein Objekt in einem Array in React?
Nutze map und gib für den Eintrag, der sich ändert, ein neues Objekt zurück und für jeden anderen den ursprünglichen Eintrag: setTodos(todos.map(t => t.id === id ? { ...t, done: !t.done } : t)).
Wie aktualisiere ich ein verschachteltes Objekt im React-State?
Kopiere jede Ebene auf dem Weg zum Feld, das du änderst: setUser({ ...user, address: { ...user.address, city: 'Oslo' } }). Ein einzelner Spread kopiert nur die oberste Ebene, also wäre das innere Objekt weiterhin geteilt.
Sollte ich für React-State structuredClone oder den Spread-Operator nutzen?
Bevorzuge Spread. Er kopiert nur, was sich ändert, und lässt die unberührten Teile dieselben Objekte bleiben, was React.memo und Vergleichen hilft. structuredClone kopiert jedes Mal alles, was einfacher zu schreiben, aber bei großem State langsamer ist und die Referenzgleichheit für unveränderte Teile zerstört.