React entdeckt Änderungen am State über Referenz-Vergleiche – nicht über Tiefen-Vergleiche. Wer ein Objekt oder Array direkt mutiert (array.push(...), obj.key = value), ändert zwar den Inhalt, aber die Referenz bleibt gleich. React denkt: „Nichts geändert" – und rendert nicht. Dieser Artikel zeigt, warum Immutability in React so wichtig ist, wie man Arrays und Objekte korrekt aktualisiert und welche Werkzeuge den Umgang erleichtern.
Das Problem in einem Beispiel
function TodoList() {
const [todos, setTodos] = useState([]);
function addTodo(text) {
todos.push({ id: crypto.randomUUID(), text });
setTodos(todos); // − gleiche Referenz – kein Re-Render
}
return (
<ul>
{todos.map((t) => <li key={t.id}>{t.text}</li>)}
<button onClick={() => addTodo('Neu')}>Hinzufügen</button>
</ul>
);
}todos.push(...) modifiziert das bestehende Array. Die Referenz todos bleibt aber dieselbe. React vergleicht alte mit neuer Referenz, sieht keinen Unterschied, rendert nicht neu. Die Liste wirkt wie eingefroren.
Die Lösung: neue Referenz erzeugen
State-Updates funktionieren in React nur, wenn beim Setzen ein neues Objekt oder Array übergeben wird.
function addTodo(text) {
setTodos((current) => [
...current,
{ id: crypto.randomUUID(), text },
]);
}[...current, ...] erzeugt ein neues Array. React vergleicht: alte Referenz ≠ neue Referenz -> Re-Render. Liste aktualisiert sich.
Arrays korrekt aktualisieren
| Operation | − Mutierend | + Immutabel |
|---|---|---|
| Hinzufügen am Ende | arr.push(x) | [...arr, x] |
| Hinzufügen am Anfang | arr.unshift(x) | [x, ...arr] |
| Entfernen | arr.splice(i, 1) | arr.filter((_, idx) => idx !== i) |
| Element ersetzen | arr[i] = x | arr.map((el, idx) => idx === i ? x : el) |
| Sortieren | arr.sort() | [...arr].sort() |
| Umkehren | arr.reverse() | [...arr].reverse() |
| Element ändern | arr[i].name = 'A' | arr.map((el, idx) => idx === i ? { ...el, name: 'A' } : el) |
Auch verschachtelte Strukturen müssen auf jeder Ebene neu gebaut werden – sonst zeigt React-DevTools zwar einen Re-Render an, aber React.memo oder useMemo-Optimierungen bemerken nichts.
Objekte korrekt aktualisieren
function updateName(newName) {
user.name = newName; // −
setUser(user);
}function updateName(newName) {
setUser((prev) => ({ ...prev, name: newName }));
}Verschachtelte Objekte
Bei mehreren Ebenen muss jede betroffene Ebene neu erzeugt werden:
// user = { name, address: { city, zip } }
function updateCity(newCity) {
setUser((prev) => ({
...prev,
address: {
...prev.address,
city: newCity,
},
}));
}Updater-Funktion vs. direkter Wert
Beim Setzen gibt es zwei Schreibweisen:
// Direkt – funktioniert oft
setCount(count + 1);
// Updater-Funktion – immer korrekt
setCount((c) => c + 1);Die Updater-Funktion bekommt den aktuellen Wert als Argument. Wichtig wird das, sobald mehrere Updates kurz hintereinander passieren oder asynchrone Callbacks im Spiel sind. Empfehlung: Im Zweifel immer die Updater-Funktion nutzen.
Tools: Immer & immerable Updates
Bei tief verschachtelten Strukturen wird der Spread-Stil schnell unübersichtlich. Die Bibliothek Immer erlaubt eine „mutierend aussehende" Schreibweise, die intern unveränderlich arbeitet:
npm install immer use-immerimport { useImmer } from 'use-immer';
function Profile() {
const [user, updateUser] = useImmer({
name: 'Anna',
address: { city: 'Berlin', zip: '10115' },
});
function changeCity(city) {
updateUser((draft) => {
draft.address.city = city; // sieht mutierend aus, ist es nicht
});
}
return (
<div>
<p>{user.name} aus {user.address.city}</p>
<button onClick={() => changeCity('Hamburg')}>Umzug</button>
</div>
);
}Immer erzeugt im Hintergrund ein neues, unveränderliches Objekt. Die Lesbarkeit bleibt aber wie bei klassischer Mutation. Redux Toolkit nutzt Immer übrigens unter der Haube.
Häufige Fehler
setState direkt nach Mutation aufrufen.
Setzt zwar den Wert, ändert aber die Referenz nicht. Re-Render bleibt aus.
Object.assign(state, …) als Update.
Mutiert das Original. Stattdessen Object.assign({}, state, …) oder einfach Spread.
Tief verschachtelten State im selben Objekt halten.
Macht das Update aufwendig. Oft ist es besser, den State flach zu halten – verschiedene useState-Aufrufe für logisch getrennte Werte.
Map/Set als State direkt mutieren.
set.add(...) ändert die Instanz. Stattdessen setMySet(new Set([...mySet, value])).
Auf abgeleitete Werte als „State" pochen.
Was berechnet werden kann, wird beim Rendern berechnet – nicht in State-Variablen gespiegelt. Spart Bugs durch falsche Synchronisierung.