JS
() =>
JavaScript · Immutability · Funktionale Programmierung · State Management
JavaScript Immutability Patterns
Unveränderliche Datenstrukturen richtig einsetzen

Mutabler State ist die häufigste Quelle schwer findbarer Bugs in JavaScript-Anwendungen. Immutability Patterns machen Zustandsänderungen explizit, ermöglichen Änderungserkennung per Referenzvergleich und verhindern, dass shared State an unerwarteten Stellen verändert wird – von Object.freeze bis zu Records und Tuples.

15 Min. Lesezeit Object.freeze · Spread · Immer.js · Records & Tuples ES2015+ · React · Redux · State Management

1. Warum mutabler State Bugs produziert

JavaScript-Objekte und Arrays werden als Referenz übergeben. Wenn man ein Objekt an eine Funktion übergibt, erhält die Funktion nicht eine Kopie, sondern einen Zeiger auf dasselbe Objekt im Speicher. Jede Mutation innerhalb der Funktion verändert das Original – auch wenn das der Aufrufer nicht erwartet. Diese unerwünschten Nebeneffekte sind eine der häufigsten Quellen schwer reproduzierbarer Bugs: der State wird an einer Stelle verändert, die Auswirkung zeigt sich aber an einer ganz anderen Stelle der Anwendung, oft viel später.

Immutability Patterns lösen dieses Problem fundamental: anstatt Objekte zu verändern, werden neue Objekte mit den geänderten Werten erzeugt. Das Original bleibt unangetastet. In React und Redux ist das nicht nur eine Empfehlung, sondern eine Grundvoraussetzung für die Funktionsfähigkeit der Bibliothek: die Change Detection basiert auf Referenzvergleich (===), nicht auf tiefem Wertevergleich. Mutiert man den State direkt, zeigt der Vergleich keine Änderung – die UI wird nicht aktualisiert. Immutability macht Zustandsänderungen explizit, vorhersehbar und testbar.

2. Object.freeze: Shallow vs. Deep Immutability

Object.freeze() ist das einfachste Immutability-Werkzeug in JavaScript. Es verhindert, dass neue Eigenschaften hinzugefügt, bestehende verändert oder gelöscht werden können. Im Strict Mode wirft ein Versuch, ein gefrorenes Objekt zu verändern, einen TypeError. Außerhalb des Strict Mode schlägt die Mutation lautlos fehl – ein typischer Fallstrick. Das ist der Grund, warum TypeScript mit Strict Mode und Linting-Regeln wie prefer-const empfohlen werden.

Der kritische Schwachpunkt von Object.freeze(): es ist shallow. Verschachtelte Objekte und Arrays sind nicht eingefroren – nur ihre Referenzen im äußeren Objekt sind unveränderlich, nicht die referenzierten Objekte selbst. Für tiefe Immutability muss man rekursiv freezeën – das ist teuer für große Objekte und kein produktionstaugliches Pattern für häufige Zustandsänderungen. Die pragmatische Lösung: Deep Freeze für Konfigurationsobjekte, die nur einmal gesetzt werden, und für Unit-Tests, um mutierenden Code zu entdecken.


// Shallow freeze — nested objects remain mutable
const config = Object.freeze({
  apiUrl: 'https://api.mironsoft.de',
  timeout: 5000,
  nested: { retries: 3 }, // NOT frozen
});

config.apiUrl = 'other'; // TypeError in strict mode, silently fails otherwise
config.nested.retries = 99; // Works! Shallow freeze doesn't protect nested objects

// Deep freeze utility — recursive, suitable for config objects
function deepFreeze(obj) {
  if (obj === null || typeof obj !== 'object') return obj;

  Object.getOwnPropertyNames(obj).forEach(name => {
    deepFreeze(obj[name]);
  });

  return Object.freeze(obj);
}

const frozenConfig = deepFreeze({
  db: { host: 'localhost', port: 5432 },
  cache: { ttl: 300, maxSize: 1000 },
});

frozenConfig.db.host = 'other'; // TypeError — deep freeze protects nested objects

// Detect frozen objects
console.log(Object.isFrozen(frozenConfig));    // true
console.log(Object.isFrozen(frozenConfig.db)); // true

3. Spread-Operator und Object.assign als Immutability-Werkzeuge

Der Spread-Operator (...) ist das meistverwendete Immutability-Werkzeug in modernem JavaScript. { ...original, key: newValue } erstellt ein neues Objekt mit allen Eigenschaften von original, wobei key überschrieben wird. Das Original bleibt unverändert. Diese Syntax ist für flache Updates elegant und verständlich – für Objekte mit zwei oder drei Ebenen Tiefe ist sie aber bereits unhandlich, weil jede Ebene einzeln gespreaded werden muss.

Ein häufiger Fehler beim Spread: er ist ebenfalls shallow. const copy = { ...original } kopiert nur die oberste Ebene. Nested Objects teilen sich immer noch dieselbe Referenz. Wer glaubt, er habe eine tiefe Kopie erstellt, und dann das kopierte Nested-Objekt verändert, mutiert das Original. Das Pattern für immutable Updates auf mehreren Ebenen kombiniert mehrere Spreads: { ...state, user: { ...state.user, name: newName } }. Das ist korrekt, wird aber schnell unleserlich bei tiefer Verschachtelung – der Punkt, an dem Immer.js oder eine Hilfsfunktion wie produce sinnvoll wird.


// Immutable update patterns with spread operator
const state = {
  user: { name: 'Max', age: 30, address: { city: 'Berlin', zip: '10115' } },
  settings: { theme: 'dark', lang: 'de' },
  cart: [{ id: 1, qty: 2 }, { id: 2, qty: 1 }],
};

// Flat update — safe and readable
const withNewTheme = { ...state, settings: { ...state.settings, theme: 'light' } };

// Deep update — correct but verbose
const withNewCity = {
  ...state,
  user: {
    ...state.user,
    address: {
      ...state.user.address,
      city: 'Hamburg',
    },
  },
};

// Array updates without mutation
const addItem    = { ...state, cart: [...state.cart, { id: 3, qty: 1 }] };
const removeItem = { ...state, cart: state.cart.filter(i => i.id !== 1) };
const updateQty  = {
  ...state,
  cart: state.cart.map(i => i.id === 2 ? { ...i, qty: i.qty + 1 } : i),
};

// Verify originals are untouched
console.log(state.settings.theme);   // 'dark' — original unchanged
console.log(state.user.address.city); // 'Berlin' — original unchanged

4. Immutable Array-Operationen ohne Mutation

JavaScript-Arrays haben mutierbare Methoden (push, pop, splice, sort, reverse) und nicht-mutierbare Methoden (map, filter, reduce, concat, slice). Das Immutability-Pattern: ausschließlich die nicht-mutierbaren Methoden verwenden und für Operationen wie Sort und Reverse zunächst eine Kopie erstellen. Seit ES2023 gibt es die Methoden toSorted(), toReversed(), toSpliced() und with(), die neue Arrays zurückgeben statt das Original zu verändern.

Der häufigste Immutability-Bug bei Arrays: array.sort() sortiert das Original in-place und gibt es zurück. Wer das Ergebnis als neue Variable speichert, hat trotzdem das Original verändert, weil beide Variablen auf dasselbe Array zeigen. Das korrekte Pattern ist [...array].sort(compareFn) oder mit ES2023 array.toSorted(compareFn). Dasselbe gilt für reverse(): immer [...array].reverse() oder array.toReversed() verwenden, nie direktes array.reverse() auf shared State.

5. Strukturelles Sharing: Effizienz ohne Kopieren

Immutability klingt teuer: bei jeder Änderung ein neues Objekt erzeugen. Für kleine Objekte ist das vernachlässigbar. Aber bei einem Redux-Store mit hundert verschachtelten Objekten wäre ein vollständiges Deep-Clone bei jeder Aktion inakzeptabel. Die Lösung ist strukturelles Sharing: unveränderte Teile des Objekt-Baums werden nicht kopiert, sondern die Referenzen werden geteilt. Nur der Pfad von der Wurzel zur geänderten Eigenschaft wird neu erstellt.

Das ist genau das, was der Spread-Operator implizit tut: { ...state, user: { ...state.user, name: 'neu' } } erstellt neue Objekte für state und state.user, aber state.settings und state.cart werden nicht kopiert – die neuen und alten Objekte teilen sich dieselben Referenzen für unveränderliche Teile. Diese geteilten Referenzen ermöglichen den schnellen Referenzvergleich: wenn newState.settings === oldState.settings, weiß eine React-Komponente, dass sich die Settings nicht geändert haben, ohne einen tiefen Wertevergleich durchführen zu müssen.


// Structural sharing — unchanged parts share references
const before = {
  users: [{ id: 1, name: 'Max' }, { id: 2, name: 'Anna' }],
  settings: { theme: 'dark' },
  metadata: { version: 3, lastUpdated: '2026-05-10' },
};

// Update only one user — settings and metadata are shared by reference
const after = {
  ...before,
  users: before.users.map(u => u.id === 1 ? { ...u, name: 'Maximilian' } : u),
};

// Verify structural sharing — identical references for unchanged subtrees
console.log(after.settings === before.settings);   // true — shared
console.log(after.metadata === before.metadata);   // true — shared
console.log(after.users === before.users);         // false — new array
console.log(after.users[1] === before.users[1]);   // true — user 2 unchanged, shared

// Change detection in O(1) — no deep comparison needed
function hasSettingsChanged(prev, next) {
  return prev.settings !== next.settings; // Reference equality suffices
}

console.log(hasSettingsChanged(before, after)); // false — same reference

6. Immer.js: Mutable schreiben, immutable erhalten

Immer.js löst das Lesbarkeits-Problem von tief verschachtelten Immutability-Updates elegant: man schreibt Code, der aussieht wie mutable Mutation, und Immer sorgt dafür, dass das Ergebnis ein neues, immutables Objekt ist. Die Funktion produce(baseState, recipe) übergibt der Recipe-Funktion einen Proxy (draft), der alle Mutationen aufzeichnet, ohne das Original zu berühren. Nach dem Ende der Recipe-Funktion berechnet Immer das neue immutable Objekt mit strukturellem Sharing.

Immer ist besonders wertvoll in Redux Toolkit, das produce intern in allen Reducer-Funktionen verwendet. Entwickler schreiben state.user.name = 'neu' statt { ...state, user: { ...state.user, name: 'neu' } }. Das macht komplexe State-Updates lesbar und fehlerfrei – Immer garantiert dabei volle Immutability des originalen State-Objekts. Der Performance-Overhead von Immer gegenüber manuellen Spreads ist bei typischen UI-State-Updates vernachlässigbar; bei sehr großen Objekten kann ein gezielter Vergleich sinnvoll sein.

7. Referenzgleichheit und Change Detection

Das wichtigste Performance-Feature von konsequenter Immutability ist schnelle Change Detection per Referenzvergleich. React.memo, useMemo, useCallback und React.PureComponent prüfen alle, ob Props oder Werte mit === gleich geblieben sind. Bei mutierten Objekten ist === immer true (dieselbe Referenz), auch wenn sich der Inhalt geändert hat – die Komponente wird nicht neu gerendert. Bei immutablen Updates ist === immer false für geänderte Objekte und true für unveränderte – exakt das, was React für korrekte, effiziente Re-Renders braucht.

Dieser Mechanismus macht Immutability zur Grundlage effizienter UI-Frameworks. Redux' connect(), Zustand's Selektoren, Recoil's Atoms – alle basieren auf der Annahme, dass unveränderter State dieselbe Referenz behält. Das ermöglicht Memoization, die tatsächlich funktioniert. Ein Selector, der mit useMemo memoized wird, muss seinen Wert nur dann neu berechnen, wenn seine Input-Referenzen sich geändert haben – das ist eine O(1)-Prüfung statt eines tiefen Wertevergleichs.

8. Records und Tuples: native Immutability (Stage 2)

Records und Tuples sind ein TC39-Proposal auf Stage 2, das native primitive Immutability in JavaScript einführt. Ein Record (mit #{ }-Syntax) ist wie ein Objekt, aber primitiv und tief immutabel – wie ein String oder eine Zahl. Zwei Records mit denselben Werten sind mit === gleich, ohne dass man eine Identitätsverwaltung braucht. Das löst ein Grundproblem von Objekt-basierter Immutability: zwei verschiedene Objekte mit identischem Inhalt sind immer !==, was Memoization und Change Detection erschwert.

Records und Tuples (mit #[ ]-Syntax) können keine normalen Objekte enthalten – alle Werte müssen primitiv oder selbst Records/Tuples sein. Das garantiert tiefe Immutability ohne Laufzeitchecks. In Maps und Sets als Keys können Records und Tuples verwendet werden – ein Feature, das mit normalen Objekten nicht möglich ist. Obwohl noch nicht in den Browsern verfügbar, signalisiert das Proposal die Richtung der JavaScript-Plattform in Bezug auf erstklassige Unterstützung für unveränderliche Datenstrukturen.

9. Immutability-Strategien im Vergleich

Die richtige Immutability-Strategie hängt vom Anwendungsfall ab: flache vs. tiefe Strukturen, Schreibhäufigkeit, Team-Erfahrung und Performance-Anforderungen. Es gibt keine universelle Antwort, aber klare Empfehlungen je nach Kontext.

Strategie Stärken Schwächen Ideal für
Object.freeze Kein Overhead, nativ Nur shallow, Fehler nur im Strict Mode Konfigurationsobjekte, Tests
Spread-Operator Lesbar, kein Overhead Verbose bei tiefer Verschachtelung Flache Updates, React-State
Immer.js Lesbar, tiefe Updates einfach Externe Abhängigkeit, Proxy-Overhead Redux-Reducer, komplexer State
structuredClone Native Deep Copy, kein Sharing O(n) – kein strukturelles Sharing Kleine Objekte, einmaliges Kopieren
Records & Tuples Wertgleichheit mit ===, primitiv Noch nicht verfügbar (Stage 2) Zukunft: Keys in Maps, Memoization

Die pragmatische Empfehlung für die meisten Projekte: Spread-Operator für flache Updates, Immer.js (via Redux Toolkit) für komplexen State mit tiefer Verschachtelung, Object.freeze für Konfigurationen und Tests. structuredClone ist nützlich, wenn man wirklich eine unabhängige Kopie ohne jegliches Sharing braucht – etwa um einen State-Snapshot für Undo-Funktionalität zu erstellen. Das Immutability-Pattern am wenigsten oft unterschätzt: die Array-Methoden toSorted(), toReversed() und with(), die seit ES2023 in allen modernen Umgebungen verfügbar sind.

Mironsoft

JavaScript-Architektur, State-Management und Frontend-Performance

Stabiles State-Management für Ihre React-Anwendung?

Wir analysieren vorhandene State-Management-Implementierungen auf Mutationsfehler und bauen Immutability-Patterns ein, die Re-Render-Verhalten, Change Detection und Testbarkeit verbessern.

State-Audit

Analyse bestehender Redux/Zustand-Stores auf Mutationsfehler und falsche Change-Detection

Reducer-Refactoring

Migration von manuellen Spread-Patterns zu Immer.js / Redux Toolkit mit strukturellem Sharing

Performance-Optimierung

Memoization und Change Detection korrekt einrichten für messbar weniger Re-Renders

10. Zusammenfassung

Immutability Patterns sind keine optionale Best Practice – sie sind die Grundlage für korrekte Change Detection, zuverlässige Memoization und vorhersehbares State-Management in modernen JavaScript-Anwendungen. Object.freeze bietet einfache shallow Immutability für Konfigurationsobjekte. Der Spread-Operator ist das meistverwendete Tool für flache immutable Updates. Strukturelles Sharing stellt sicher, dass Immutability nicht auf Kosten der Performance geht – unveränderte Objekte teilen ihre Referenzen. Immer.js macht tiefe Updates lesbar ohne die Vorteile von Immutability aufzugeben.

Der wichtigste Perspektivwechsel: Immutability ist kein Performance-Problem, sondern eine Performance-Lösung. React.memo und useMemo funktionieren korrekt nur mit immutablen Updates. Arrays mit toSorted(), toReversed() und with() machen mutierende Array-Methoden obsolet. Records und Tuples werden Wertgleichheit mit === für Objekte ermöglichen – ein Paradigmenwechsel für State Management und Memoization in JavaScript.

Immutability Patterns — Das Wichtigste auf einen Blick

Fehler durch Mutation vermeiden

Objekte als Referenz übergeben und mutieren verursacht schwer findbare Bugs. Spread-Operator und Immer.js erzeugen neue Objekte, ohne das Original zu berühren.

Strukturelles Sharing

Unveränderte Teile des Objektbaums teilen Referenzen. O(1) Change Detection mit === – keine Deep-Comparison nötig für React, Redux und Memoization.

Array-Immutability ES2023

toSorted(), toReversed(), toSpliced() und with() geben neue Arrays zurück. Nie direkt sort() oder reverse() auf shared State anwenden.

Immer.js für komplexen State

produce(state, draft => { draft.x.y = z }) – mutable Syntax, immutables Ergebnis. Kernstück von Redux Toolkit, vollständiges strukturelles Sharing.

11. FAQ: JavaScript Immutability Patterns

1Warum ist Immutability in React wichtig?
React nutzt Referenzvergleich für Change Detection. Mutierter State hat dieselbe Referenz – kein Re-Render. Immutable Updates erzeugen neue Referenzen für korrekte Renders.
2Object.freeze vs. Spread-Operator?
freeze verhindert Mutationen am vorhandenen Objekt (shallow). Spread erzeugt ein neues Objekt – das Original bleibt unangetastet. Für Updates: Spread. Für Konfiguration: freeze.
3Ist Immutability nicht teuer?
Strukturelles Sharing verhindert unnötiges Kopieren. O(1) Change Detection durch Referenzvergleich kompensiert den geringen Overhead für neue Objekte.
4Wann Immer.js statt Spread?
Ab zwei Ebenen Tiefe wird manueller Spread unlesbar. Immer.js für komplexe Reducer und Redux – Redux Toolkit integriert Immer bereits intern.
5Was ist strukturelles Sharing?
Unveränderte Teile des Objektbaums teilen Referenzen zwischen altem und neuem State. Gleiche Referenz = keine Änderung – O(1) Change Detection ohne Deep-Compare.
6const vs. Object.freeze?
const verhindert Neuzuweisung der Variable, nicht Mutation des Objekts. freeze verhindert Mutation, nicht Neuzuweisung. Beide zusammen: maximale Sicherheit.
7Array.sort() Mutationsfalle vermeiden?
sort() mutiert in-place. [...array].sort(fn) oder ES2023 array.toSorted(fn) – immer neues Array, kein Mutieren des Originals.
8Was sind Records und Tuples?
TC39 Stage 2: primitive Datenstrukturen (#{ } und #[ ]), tief immutabel, wertbasierte Gleichheit mit ===. Zwei Records mit gleichen Werten sind ===gleich.
9structuredClone vs. Spread?
structuredClone: vollständige tiefe Kopie, kein Sharing. Für Snapshots und Undo. Für häufige State-Updates zu teuer – Spread mit strukturellem Sharing bevorzugen.
10Team gegen Mutationen schützen?
ESLint no-param-reassign, TypeScript Readonly<T>, Object.freeze in Tests. Immer.js wirft in Dev-Mode bei Mutationen außerhalb des Draft.