Zustand Middleware: persist und devtools praktisch einsetzen
AI generated
{ }
React 19 · Zustand · State Management
Zustand-Middleware praktisch einsetzen
persist und devtools kombinieren, ohne den Storage aufzublaehen oder DevTools zu verwirren

Ein Zustand-Store bleibt ohne Middleware fluechtig und schwer zu debuggen. persist und devtools loesen genau diese beiden Probleme, sobald man weiss, wie sie kombiniert und mit partialize gezielt eingeschraenkt werden.

13 Min. Lesezeit Zustand · persist · devtools State Management

1. Was Zustand-Middlewares ueberhaupt sind

Eine Zustand-Middleware ist eine Funktion, die den eigentlichen Store-Creator umschliesst und zusaetzliches Verhalten injiziert, ohne die fachliche Store-Logik selbst zu veraendern. persist ergaenzt Speicherung und Wiederherstellung des States, devtools ergaenzt eine Anbindung an die Redux-DevTools-Browsererweiterung, beide arbeiten dabei transparent im Hintergrund der eigentlichen State-Updates, ohne dass die aufrufenden Komponenten davon ueberhaupt etwas mitbekommen.

Im Vergleich zu Redux, wo Middleware ueber einen eigenen Store-Enhancer-Mechanismus mit vergleichsweise viel Boilerplate eingebunden wird, ist eine Zustand-Middleware schlicht eine Funktion hoeherer Ordnung, die um create() gelegt wird. Das macht Middlewares minimal invasiv, leicht kombinierbar und einfach wieder entfernbar, ohne den Rest des Stores anfassen zu muessen. Fuer ein bestehendes Projekt bedeutet das, dass Persistenz oder DevTools-Anbindung meist ohne grosse Refaktorierung nachtraeglich ergaenzt werden koennen, weil sich am eigentlichen Store-Interface nichts aendert.

2. persist im Grundaufbau

persist(config, { name, storage }) speichert den Store-State automatisch in localStorage, oder alternativ in sessionStorage beziehungsweise einem beliebigen eigenen Storage-Adapter. Die Option name bestimmt den Schluessel, unter dem der serialisierte State im Storage abgelegt wird, sie sollte projektweit eindeutig sein, um Kollisionen mit anderen Stores zu vermeiden.

Beim Start der Anwendung liest persist automatisch aus dem Storage und rehydriert den Store, bevor die erste Komponente ueberhaupt rendert. Ein eigener useEffect, der beim Mount manuell aus localStorage liest und den Store befuellt, ist damit nicht mehr noetig, dieser komplette Ablauf ist bereits in der Middleware gekapselt.


import { create } from 'zustand';
import { persist } from 'zustand/middleware';

const useSettingsStore = create(
  persist(
    (set) => ({
      theme: 'light',
      setTheme: (theme) => set({ theme }),
    }),
    { name: 'settings-storage' }
  )
);

3. partialize fuer selektive Persistenz

partialize(state) erlaubt es, nur einen Teil des States zu persistieren, etwa dauerhafte Benutzereinstellungen statt fluechtiger UI-Flags wie isModalOpen oder activeTab. Ohne partialize landet standardmaessig der komplette Store im Storage, inklusive Werten, die beim naechsten Laden ohnehin auf ihren Ausgangswert zurueckgesetzt werden sollten.

Praktisch wird partialize meist als Funktion definiert, die ein neues Objekt mit nur den gewuenschten Schluesseln zurueckgibt. In TypeScript laesst sich der Rueckgabetyp ueber Partial absichern, sodass ein versehentlich vergessenes oder falsch benanntes Feld bereits beim Kompilieren auffaellt statt erst zur Laufzeit im Storage zu fehlen.


const useAppStore = create(
  persist(
    (set) => ({
      theme: 'light',
      language: 'de',
      isModalOpen: false,
      activeTab: 'overview',
      setTheme: (theme) => set({ theme }),
    }),
    {
      name: 'app-storage',
      // Nur dauerhafte Einstellungen persistieren,
      // fluechtige UI-Flags bleiben aussen vor
      partialize: (state) => ({
        theme: state.theme,
        language: state.language,
      }),
    }
  )
);

4. devtools und die Redux DevTools Extension

devtools(config, { name }) verbindet den Store mit der Redux-DevTools-Browsererweiterung. Jede State-Aenderung erscheint dort als eigener Eintrag im Zeitstrahl, inklusive der Moeglichkeit, per Time-Travel-Debugging zu einem frueheren State-Snapshot zurueckzuspringen und die Anwendung in genau diesem Zustand zu inspizieren.

Fuer aussagekraeftige Eintraege im Zeitstrahl sollten set-Aufrufe mit einem zweiten und dritten Argument versehen werden, etwa set(newState, false, 'todos/add'). Ohne diesen Namen erscheinen in den DevTools nur generische, anonyme Actions, was das Nachvollziehen einer Fehlerursache in einem groesseren Store erheblich erschwert.


import { devtools } from 'zustand/middleware';

const useTodoStore = create(
  devtools(
    (set) => ({
      todos: [],
      addTodo: (text) =>
        set(
          (state) => ({ todos: [...state.todos, { id: crypto.randomUUID(), text }] }),
          false,
          'todos/add'
        ),
    }),
    { name: 'TodoStore' }
  )
);

5. Mehrere Middlewares kombinieren

Middlewares werden verschachtelt angewendet, ein uebliches Muster ist devtools(persist(immer(storeCreator))). Die Reihenfolge dieser Verschachtelung ist nicht beliebig, sie beeinflusst sowohl, was in den DevTools sichtbar wird, als auch, wie der State vor dem Speichern in persist transformiert wird.

Eine ungeschickte Reihenfolge, etwa persist ausserhalb von devtools statt innerhalb, kann dazu fuehren, dass die DevTools den bereits serialisierten oder rehydrierten State nicht korrekt oder mit falschen Action-Namen anzeigen. In TypeScript wird die Typinferenz bei mehreren verschachtelten Middlewares zudem spuerbar komplexer und profitiert von expliziten StateCreator-Typannotationen an den jeweiligen Store-Slices, insbesondere sobald zusaetzlich noch immer als dritte Middleware in derselben Kette eingesetzt wird.


import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';

const useStore = create(
  devtools(
    persist(
      immer((set) => ({
        todos: [],
        addTodo: (text) =>
          set((state) => {
            state.todos.push({ id: crypto.randomUUID(), text });
          }),
      })),
      { name: 'todo-storage' }
    ),
    { name: 'TodoStore' }
  )
);

6. Versionierung und Migration gespeicherter States

Die Optionen version und migrate in persist erlauben es, gespeicherte States aus aelteren App-Versionen beim Laden in das aktuelle Schema zu ueberfuehren, statt sie zu verwerfen oder einen Laufzeitfehler zu riskieren, wenn erwartete Felder fehlen.

migrate(persistedState, version) erhaelt den alten State zusammen mit dessen gespeicherter Versionsnummer und gibt den transformierten, aktuellen State zurueck. Das ist besonders relevant, wenn ein Feld umbenannt, ein neues Pflichtfeld eingefuehrt oder die Struktur eines verschachtelten Objekts geaendert wurde, ohne dass Nutzer mit aelterem, lokal gespeichertem State beim naechsten Besuch einen defekten Store erhalten. Wird die Versionsnummer bei jeder inkompatiblen Schema-Aenderung konsequent erhoeht, laesst sich diese Migration zudem in kleinen, gut nachvollziehbaren Schritten statt in einem einzigen grossen Sprung durchfuehren.


const useSettingsStore = create(
  persist(
    (set) => ({
      themeMode: 'light',
      setThemeMode: (themeMode) => set({ themeMode }),
    }),
    {
      name: 'settings-storage',
      version: 2,
      migrate: (persistedState, version) => {
        if (version === 1) {
          // Altes Feld 'theme' wurde zu 'themeMode' umbenannt
          return { themeMode: persistedState.theme ?? 'light' };
        }
        return persistedState;
      },
    }
  )
);

7. Eigene Storage-Adapter einsetzen

Die storage-Option akzeptiert jeden Adapter, der die Methoden getItem, setItem und removeItem implementiert. Damit laesst sich statt localStorage beispielsweise AsyncStorage in React Native einsetzen, oder ein verschluesselnder Wrapper um localStorage, wenn sensible Daten lokal abgelegt werden muessen.

createJSONStorage(() => customStorageEngine) kapselt die JSON-Serialisierung, ein eigener Adapter muss deshalb nur die rohe Storage-Schnittstelle bedienen, die Umwandlung zwischen JavaScript-Objekt und String uebernimmt Zustand automatisch. Das reduziert eigenen Code auf die reine Speicherlogik, ohne sich um Serialisierungsdetails kuemmern zu muessen.


import { createJSONStorage, persist } from 'zustand/middleware';

const encryptedStorage = {
  getItem: (name) => {
    const raw = localStorage.getItem(name);
    return raw ? decrypt(raw) : null;
  },
  setItem: (name, value) => localStorage.setItem(name, encrypt(value)),
  removeItem: (name) => localStorage.removeItem(name),
};

const useSecureStore = create(
  persist(
    (set) => ({ token: null, setToken: (token) => set({ token }) }),
    { name: 'secure-storage', storage: createJSONStorage(() => encryptedStorage) }
  )
);

8. Hydration-Timing bei Server-Side-Rendering

Bei Server-Side-Rendering, etwa mit Next.js, existiert beim ersten Server-Render kein localStorage. persist markiert den Store deshalb zunaechst als nicht hydriert, der eigentliche Rehydrierungs-Zeitpunkt auf dem Client laesst sich ueber die Callback-Option onRehydrateStorage abfangen.

Eine verbreitete Loesung ist ein zusaetzliches hasHydrated-Flag im Store, das erst nach erfolgreichem Rehydrieren auf true gesetzt wird. Komponenten koennen so bis dahin einen Platzhalter oder Loading-Zustand rendern, statt einen kurzzeitigen Server-Client-Mismatch anzuzeigen, der sonst zu einem sichtbaren Flackern der Werte fuehren wuerde. Gerade bei theme-abhaengigen Werten faellt ein solcher Mismatch sofort optisch auf, weshalb sich der zusaetzliche Aufwand fuer das Flag in produktiven Anwendungen fast immer lohnt.


const useAppStore = create(
  persist(
    (set) => ({
      hasHydrated: false,
      theme: 'light',
      setHasHydrated: (value) => set({ hasHydrated: value }),
    }),
    {
      name: 'app-storage',
      onRehydrateStorage: () => (state) => {
        state?.setHasHydrated(true);
      },
    }
  )
);

function ThemeSwitcher() {
  const hasHydrated = useAppStore((s) => s.hasHydrated);
  if (!hasHydrated) return <Skeleton />;
  return <ThemeToggle />;
}

9. Haeufige Fehler bei Zustand-Middleware

Der haeufigste Fehler ist fehlendes partialize, wodurch der komplette Store inklusive temporaerer UI-Flags im localStorage landet. Das blaeht den Storage unnoetig auf und stellt beim naechsten Laden mitunter unerwartete UI-Zustaende wieder her, etwa ein Modal, das beim Neuladen ploetzlich wieder offen ist, obwohl der Nutzer es laengst geschlossen hatte.

Ein zweiter verbreiteter Fehler ist eine ungeeignete Middleware-Reihenfolge oder das Vergessen von Versionierung bei Schema-Aenderungen. Wird eine version-Erhoehung ohne passende migrate-Funktion vergessen, fuehren alte, bereits persistierte States nach einem Deployment zu Laufzeitfehlern, weil erwartete Felder im gespeicherten Objekt schlicht fehlen.

Middleware Zweck Typische Stolperfalle Kombinierbar mit
persist State ueber Seiten-Reloads hinweg in localStorage oder eigenem Storage sichern Fehlendes partialize blaeht den Storage mit fluechtigen UI-Flags auf devtools, immer, subscribeWithSelector
devtools Anbindung an Redux DevTools inklusive Time-Travel-Debugging Set-Aufrufe ohne Action-Namen erscheinen als generische, unlesbare Eintraege persist, immer
immer Mutierbar wirkende Schreibweise fuer verschachtelte State-Updates Vergessene return-Anweisung bei Nicht-Draft-Rueckgaben in komplexen Updates persist, devtools
subscribeWithSelector Gezieltes Abonnieren einzelner State-Ausschnitte ausserhalb von React Fehlende equalityFn fuehrt zu unnoetig haeufigen Callback-Aufrufen persist, devtools, immer

Mironsoft

React-Architektur, Performance und Magento-Frontend-Integration

React-Frontends, die schnell bleiben statt mit jedem Feature langsamer zu werden?

Wir prüfen bestehende React-Anwendungen auf unnötige Re-Renders, aufgeblähte Bundles und fragile State-Verwaltung und bauen daraus ein Frontend, das performant bleibt und sich sauber an Magento oder andere Backends anbindet.

Performance-Audit

Re-Renders, Bundle-Größe und Ladezeiten systematisch messen und beheben.

State-Architektur

Context, Zustand und Server State sauber trennen statt alles in einen Topf zu werfen.

Magento-Integration

GraphQL- oder REST-Anbindung an Magento robust und typsicher aufbauen.

10. Zusammenfassung

Zustand Middleware persist und devtools: Das Wichtigste auf einen Blick

persist

Speichert und rehydriert den Store automatisch aus localStorage oder einem beliebigen Storage-Adapter.

partialize

Beschraenkt die Persistenz auf dauerhafte Felder, fluechtige UI-Flags bleiben ausserhalb des Storage.

devtools

Verbindet den Store mit Redux DevTools, benannte Actions machen den Zeitstrahl aussagekraeftig.

Middleware-Reihenfolge

Verschachtelung wie devtools(persist(immer(...))) beeinflusst Sichtbarkeit und Serialisierung direkt.

11. FAQ: Zustand Middleware persist und devtools: Das Wichtigste auf einen Blick

1Was macht eine Zustand-Middleware genau?
Eine Middleware umschliesst den Store-Creator als Funktion hoeherer Ordnung und injiziert zusaetzliches Verhalten wie Persistenz oder DevTools-Anbindung, ohne die fachliche Store-Logik selbst zu veraendern.
2Wo speichert persist den State standardmaessig?
Standardmaessig in localStorage unter dem in der Option name angegebenen Schluessel. Alternativ laesst sich ueber die storage-Option sessionStorage oder ein eigener Adapter einsetzen.
3Warum sollte ich partialize verwenden statt den gesamten Store zu persistieren?
Ohne partialize landet der komplette Store im Storage, inklusive fluechtiger UI-Flags, die beim naechsten Laden ohnehin zurueckgesetzt werden sollten. partialize beschraenkt die Persistenz auf tatsaechlich dauerhafte Felder und haelt den Storage klein.
4Wie mache ich Eintraege in den Redux DevTools aussagekraeftig?
Indem set-Aufrufe mit einem zweiten und dritten Argument versehen werden, etwa set(newState, false, 'todos/add'). Ohne diesen Namen erscheinen in den DevTools nur generische, anonyme Actions.
5Spielt die Reihenfolge beim Verschachteln mehrerer Middlewares eine Rolle?
Ja. Die Reihenfolge beeinflusst, was in den DevTools sichtbar wird und wie der State vor dem Speichern transformiert wird. Eine ungeschickte Reihenfolge kann dazu fuehren, dass DevTools den bereits serialisierten State falsch anzeigen.
6Wozu dienen version und migrate in persist?
Sie erlauben es, gespeicherte States aus aelteren App-Versionen beim Laden in das aktuelle Schema zu ueberfuehren. migrate erhaelt den alten State und dessen Versionsnummer und gibt den transformierten, aktuellen State zurueck.
7Kann ich persist auch mit React Native oder einem eigenen Storage einsetzen?
Ja, die storage-Option akzeptiert jeden Adapter, der getItem, setItem und removeItem implementiert, etwa AsyncStorage in React Native oder einen verschluesselnden Wrapper um localStorage.
8Wie gehe ich mit Server-Side-Rendering um, wenn persist verwendet wird?
Ueber ein zusaetzliches hasHydrated-Flag, das erst nach erfolgreichem Rehydrieren im Callback onRehydrateStorage auf true gesetzt wird. Komponenten koennen bis dahin einen Platzhalter statt eines Server-Client-Mismatches rendern.
9Was passiert, wenn ich eine Versionserhoehung ohne migrate-Funktion vornehme?
Alte, bereits persistierte States koennen dann nach einem Deployment zu Laufzeitfehlern fuehren, weil vom aktuellen Code erwartete Felder im gespeicherten Objekt schlicht fehlen. Eine passende migrate-Funktion verhindert das.
10Kann ich mehrere Middlewares gleichzeitig einsetzen, etwa persist und immer zusammen?
Ja, das ist ein uebliches Muster, zum Beispiel devtools(persist(immer(storeCreator))). Wichtig ist dabei nur, die Verschachtelungsreihenfolge bewusst zu waehlen, weil sie Sichtbarkeit in DevTools und Serialisierung beeinflusst.