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

TypeScript − Falsch – State wird mutiert
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.

TypeScript + Richtig – neues Array
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 Endearr.push(x)[...arr, x]
Hinzufügen am Anfangarr.unshift(x)[x, ...arr]
Entfernenarr.splice(i, 1)arr.filter((_, idx) => idx !== i)
Element ersetzenarr[i] = xarr.map((el, idx) => idx === i ? x : el)
Sortierenarr.sort()[...arr].sort()
Umkehrenarr.reverse()[...arr].reverse()
Element ändernarr[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

TypeScript − Mutierend
function updateName(newName) {
    user.name = newName; // −
    setUser(user);
}
TypeScript + Spread mit neuer Referenz
function updateName(newName) {
    setUser((prev) => ({ ...prev, name: newName }));
}

Verschachtelte Objekte

Bei mehreren Ebenen muss jede betroffene Ebene neu erzeugt werden:

TypeScript Verschachtelt
// 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:

TypeScript Direkt vs. Updater-Funktion
// 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:

bash Installation
npm install immer use-immer
TypeScript useImmer-Beispiel
import { 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])).

/ Weiter

Zurück zu State

Zur Übersicht