Back

Unveränderlicher State leicht gemacht: Immer verstehen

Unveränderlicher State leicht gemacht: Immer verstehen

Die Aktualisierung von verschachteltem State in JavaScript ohne Mutation ist mühsam. Man muss Objekte auf jeder Ebene spreaden, nachverfolgen, welche Referenzen sich geändert haben, und hoffen, dass man nicht versehentlich etwas mutiert hat. Für eine einfache verschachtelte Aktualisierung schreibt man möglicherweise fünf Zeilen sorgfältiger Kopierlogik.

Immer löst dieses Problem mit einem anderen Ansatz: Man schreibt Code, der aussieht wie Mutation, aber automatisch unveränderlichen State erzeugt. Dieser Artikel erklärt, wie Immers Proxy-basierte Unveränderlichkeit funktioniert, warum Redux Toolkit es intern verwendet und welche praktischen Einschränkungen man vor der Einführung kennen sollte.

Wichtigste Erkenntnisse

  • Immer ermöglicht es, Code im Mutations-Stil zu schreiben, der durch seine produce-Funktion und Proxy-basierte Drafts unveränderlichen State erzeugt
  • Structural Sharing stellt sicher, dass nur modifizierte Zweige neue Referenzen erhalten, wodurch die Performance für React- und Redux-Re-Render-Checks erhalten bleibt
  • Redux Toolkit verwendet Immer intern, sodass createSlice-Reducer automatisch Unveränderlichkeit handhaben, ohne zusätzliche Imports
  • Achten Sie auf häufige Fallstricke: Weisen Sie den Draft selbst nicht neu zu, vermeiden Sie das Mischen von Draft-Mutationen mit Rückgabewerten und seien Sie vorsichtig mit Klasseninstanzen

Wie Immer unveränderlichen State erzeugt

Immers Kern-API ist die produce-Funktion. Man übergibt ihr den aktuellen State und eine „Rezept”-Funktion. Innerhalb dieses Rezepts erhält man einen draft – einen Proxy, der den ursprünglichen State umschließt. Man modifiziert den Draft mit normalen JavaScript-Mutationen. Wenn das Rezept abgeschlossen ist, generiert Immer einen neuen unveränderlichen State, der die Änderungen widerspiegelt.

import { produce } from "immer"

const baseState = {
  user: { name: "Alice", settings: { theme: "dark" } }
}

const nextState = produce(baseState, draft => {
  draft.user.settings.theme = "light"
})

// baseState.user.settings.theme ist immer noch "dark"
// nextState.user.settings.theme ist "light"

Das mentale Modell ist einfach: Man tut so, als würde man mutieren, aber Immer kümmert sich hinter den Kulissen um die unveränderlichen Aktualisierungen.

Proxy-basierte Unveränderlichkeit und Structural Sharing

Immer verwendet JavaScript-Proxy-Objekte, um Lese- und Schreibzugriffe auf den Draft abzufangen. Wenn man auf eine verschachtelte Eigenschaft zugreift, erstellt Immer lazy einen Proxy für diesen Pfad. Wenn man in eine Eigenschaft schreibt, markiert Immer diesen Knoten (und seine Vorfahren) als modifiziert und erstellt nur dort flache Kopien, wo es nötig ist.

Dieser Copy-on-Write-Ansatz ermöglicht Structural Sharing. Unveränderte Teile des State-Baums bleiben dieselben Objektreferenzen. Nur modifizierte Zweige erhalten neue Referenzen. Dies ist wichtig für React und Redux, da Referenzgleichheitsprüfungen bestimmen, ob Komponenten neu gerendert werden.

Modernes Immer (v10+) erfordert native Proxy-Unterstützung – es gibt kein ES5-Fallback. Dies ist für aktuelle Browser und Node.js in Ordnung, aber erwähnenswert, wenn man ungewöhnliche Umgebungen anvisiert.

Redux Toolkit Immer-Integration

Wenn Sie Redux Toolkit verwenden, nutzen Sie bereits Immer. RTKs createSlice umschließt Ihre Reducer-Logik automatisch mit produce. Sie schreiben „mutierenden” Code in Case-Reducern, und RTK kümmert sich um die Unveränderlichkeit:

import { createSlice } from "@reduxjs/toolkit"

const todosSlice = createSlice({
  name: "todos",
  initialState: [],
  reducers: {
    addTodo: (state, action) => {
      state.push(action.payload) // Sieht aus wie Mutation, ist aber sicher
    },
    toggleTodo: (state, action) => {
      const todo = state.find(t => t.id === action.payload)
      if (todo) todo.completed = !todo.completed
    }
  }
})

Diese Redux Toolkit Immer-Integration eliminiert das Spread-Operator-Boilerplate, das Vanilla-Redux-Reducer verbose gemacht hat. Sie müssen produce nicht separat importieren – RTK konfiguriert es für Sie.

Auto-Freezing und Iterationsverhalten

Immer friert den erzeugten State standardmäßig automatisch ein (mit Object.freeze). Dies fängt versehentliche Mutationen während der Entwicklung ab, fügt aber Overhead hinzu. Sie können es in der Produktion über die Konfiguration deaktivieren, wenn Profiling zeigt, dass es wichtig ist:

import { setAutoFreeze } from "immer"

setAutoFreeze(false) // In Produktion für Performance deaktivieren

Modernes Immer verwendet standardmäßig loose iteration für Performance: Nur aufzählbare String-Schlüssel werden in Drafts iteriert. Dies ist normalerweise das, was man für Anwendungs-State möchte, aber es bedeutet, dass Symbol-Schlüssel und nicht-aufzählbare Eigenschaften übersprungen werden, es sei denn, man aktiviert explizit strikte Iteration. Dies ist hauptsächlich in Grenzfällen oder bei Low-Level-Datenmanipulation relevant.

Praktische Einschränkungen für unveränderliche JavaScript-Updates

Mehrere Fallstricke lassen Entwickler stolpern, die neu bei Immer sind:

Weisen Sie den Draft selbst nicht neu zu. Das Modifizieren von draft.property funktioniert. Das Neuzuweisen von draft = newValue bewirkt nichts Nützliches, da Sie nur die lokale Parameter-Bindung ändern.

Rückgabewerte sind wichtig. Wenn Ihr Rezept einen Wert zurückgibt, verwendet Immer diesen als neuen State anstelle des modifizierten Drafts. Die Rückgabe von undefined wird genauso behandelt wie die Rückgabe von nichts, aber das Mischen von Draft-Mutationen und expliziten Rückgaben ist eine häufige Fehlerquelle – verwenden Sie einen Ansatz oder den anderen.

Klassen und exotische Objekte erfordern Vorsicht. Immer funktioniert am besten mit einfachen Objekten, Arrays, Maps und Sets. Klasseninstanzen werden nicht automatisch korrekt geproxied. Sie müssen sie möglicherweise als unveränderlich markieren oder separat behandeln (wie in der offiziellen Pitfalls-Dokumentation beschrieben).

Performance ist nicht kostenlos. Immer eignet sich für typischen UI-State – Formulare, Listen, Benutzerpräferenzen. Für heiße Schleifen, die Tausende von Elementen pro Frame verarbeiten, sollten Sie messen, bevor Sie annehmen, dass Immer keinen Overhead hinzufügt. Die Proxy-Maschinerie hat Kosten.

Baumförmiger State funktioniert am besten. Immer geht davon aus, dass Ihr State ein Baum ist. Zirkuläre Referenzen oder gemeinsam genutzte Objektreferenzen über Zweige hinweg können unerwartete Ergebnisse liefern.

Fazit

Immer glänzt bei mäßig komplexen, verschachtelten State-Updates – genau das, was man in React-Komponenten und Redux-Reducern antrifft. Es eliminiert Boilerplate, fängt versehentliche Mutationen ab und integriert sich nahtlos mit Redux Toolkit.

Für einfachen flachen State funktionieren native Spread-Operatoren gut. Für performance-kritische Datenverarbeitung sollten Sie zuerst Benchmarks durchführen. Aber für typisches Frontend-State-Management bietet Immers Proxy-basierter Ansatz die beste Balance aus Entwicklererfahrung und Korrektheit.

FAQs

Ja. Sie können Ihre State-Updates direkt mit produce umschließen oder Immers useImmer-Hook aus dem use-immer-Paket verwenden. Dies gibt Ihnen die gleiche Mutations-Stil-Syntax für lokalen Komponenten-State, die Redux Toolkit für globalen State bereitstellt.

Immer hat ausgezeichnete TypeScript-Unterstützung. Die produce-Funktion leitet Typen automatisch aus Ihrem Base-State ab, und Draft-Objekte behalten die richtige Typisierung bei. Sie erhalten vollständige Autovervollständigung und Typprüfung, während Sie Code im Mutations-Stil schreiben.

Immer unterstützt Maps und Sets nativ. Sie können Standard-Map- und Set-Methoden wie set, delete und add direkt auf Drafts in modernen Versionen ohne zusätzliche Konfiguration verwenden.

Nur wenn Profiling zeigt, dass es Performance-Probleme verursacht. Auto-Freeze hilft, Bugs zu fangen, indem es Fehler bei versehentlichen Mutationen wirft. Für die meisten Anwendungen ist der Overhead vernachlässigbar, aber hochfrequente Updates können davon profitieren, es über setAutoFreeze false zu deaktivieren.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay