React & State

Zustand Middleware mit Claude Code: persist, devtools, immer 2026

📅 5. Mai 2026 ⏱ 10 min Lesezeit 🤖 Claude Code

Zustand ist schon in der Basis-Version ein elegantes State-Management-Tool. Aber erst die Middleware-API macht es wirklich produktionstauglich: persist synchronisiert den State mit localStorage, devtools öffnet das Redux DevTools-Panel, immer erlaubt direkte Mutations statt verschachtelter Spreads, und subscribeWithSelector ermöglicht granulare Reaktionen auf Slice-Änderungen. Claude Code kennt alle diese Patterns – dieses Tutorial zeigt dir, wie du sie gemeinsam mit dem KI-Coding-Assistenten professionell einsetzt.

persist 1. persist Middleware – State in localStorage speichern

Grundprinzip

Warum persist?

Ohne Persistenz verliert deine App bei jedem Reload den kompletten Store-Inhalt. Die persist-Middleware von Zustand serialisiert den State automatisch in einen konfigurierbaren Storage-Backend (localStorage, sessionStorage, IndexedDB oder eigene Adapter) und rehydriert ihn beim nächsten App-Start.

Claude Code versteht dabei nicht nur die API – es schlägt dir direkt vor, welche State-Slices du persistieren solltest und welche besser im flĂĽchtigen Arbeitsspeicher bleiben (z. B. UI-Flags oder temporäre Ladestatuse).

Basis-Setup

Minimale persist-Konfiguration

TypeScript import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; interface AuthState { token: string | null; userId: string | null; setAuth: (token: string, userId: string) => void; clearAuth: () => void; } export const useAuthStore = create<AuthState>()( persist( (set) => ({ token: null, userId: null, setAuth: (token, userId) => set({ token, userId }), clearAuth: () => set({ token: null, userId: null }), }), { name: 'auth-storage', // Key in localStorage storage: createJSONStorage(() => localStorage), } ) );
partialize

Nur bestimmte Felder persistieren mit partialize

Oft willst du nicht den gesamten Store speichern – UI-Status, temporäre Fehler oder Ladezustände gehören nicht in localStorage. Mit partialize gibst du exakt an, welche State-Teile serialisiert werden:

TypeScript interface UserPreferencesState { theme: 'light' | 'dark' | 'system'; language: string; sidebarOpen: boolean; // flüchtig – nicht persistieren! isLoading: boolean; // flüchtig – nicht persistieren! setTheme: (t: UserPreferencesState['theme']) => void; } export const usePrefsStore = create<UserPreferencesState>()( persist( (set) => ({ theme: 'system', language: 'de', sidebarOpen: false, isLoading: false, setTheme: (theme) => set({ theme }), }), { name: 'user-prefs', // Nur theme + language in localStorage speichern partialize: (state) => ({ theme: state.theme, language: state.language, }), } ) );
onRehydrateStorage

Migrations und Rehydrierung mit onRehydrateStorage

Wenn sich das State-Schema zwischen App-Versionen ändert, brauchst du Migrationspfade. Claude Code hilft dir, versionierte Schemas zu schreiben und mit onRehydrateStorage sauber zu reagieren, sobald der alte State aus localStorage geladen wird:

TypeScript persist( (set) => ({ /* ... */ }), { name: 'app-state', version: 2, // Erhöhen triggert migrate() migrate: (persistedState: unknown, version: number) => { if (version === 0) { // v0 → v1: Feld umbenennen const s = persistedState as any; return { ...s, language: s.locale ?? 'de' }; } return persistedState; }, onRehydrateStorage: () => (state, error) => { if (error) { console.error('Rehydrierung fehlgeschlagen:', error); } else { console.log('State erfolgreich rehydriert:', state); } }, } )
Claude Code Prompt-Tipp: "Erstelle einen Zustand-Store mit persist-Middleware fĂĽr Auth-Daten. Persistiere nur token und userId, nicht den Loading-State. FĂĽge onRehydrateStorage hinzu und eine migrate-Funktion fĂĽr Version 2."
Achtung – SSR: localStorage existiert in Node.js nicht. Für Next.js App Router oder SSR-Frameworks musst du den Storage-Aufruf in createJSONStorage(() => localStorage) lazy evaluieren oder auf sessionStorage mit Server-Side-Fallback ausweichen.

devtools 2. devtools Middleware – Redux DevTools Integration

Grundprinzip

Redux DevTools mit Zustand nutzen

Die devtools-Middleware verbindet deinen Zustand-Store mit den Redux DevTools Browser-Extensions. Du bekommst damit Time-Travel Debugging, Action-History und State-Snapshots – ohne ein einziges Redux-Package installieren zu müssen.

Claude Code fĂĽgt die Middleware automatisch ein und benennt Aktionen nach deinen Store-Methoden, sodass das DevTools-Panel sofort lesbare Action-Namen zeigt.

Setup

devtools aktivieren

TypeScript import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; interface CartState { items: { id: string; qty: number }[]; total: number; addItem: (id: string, qty: number, price: number) => void; removeItem: (id: string) => void; clearCart: () => void; } export const useCartStore = create<CartState>()( devtools( (set) => ({ items: [], total: 0, // Action-Namen explizit setzen → erscheinen in DevTools addItem: (id, qty, price) => set( (state) => ({ items: [...state.items, { id, qty }], total: state.total + qty * price, }), false, // replace=false → merge 'cart/addItem' // Action-Label im DevTools-Panel ), removeItem: (id) => set( (state) => ({ items: state.items.filter((i) => i.id !== id), }), false, 'cart/removeItem' ), clearCart: () => set({ items: [], total: 0 }, false, 'cart/clearCart'), }), { name: 'CartStore', // Name im DevTools-Dropdown enabled: process.env.NODE_ENV !== 'production', } ) );
Time-Travel

Time-Travel Debugging in der Praxis

Mit den Redux DevTools kannst du jeden vergangenen State-Snapshot anspringen. Klicke auf eine Action in der History-Liste und die gesamte UI rendert in den damaligen Zustand – ohne die App neu zu laden. Für komplexe Warenkorb-Logiken oder Wizard-Flows ist das unschätzbar wertvoll.

Claude Code hilft dir außerdem, den dritten Parameter von set() konsequent zu befüllen. Der KI-Assistent schlägt dabei eine einheitliche Namenskonvention vor (sliceName/actionName), die in der DevTools-History sofort verständlich ist.

Best Practice: Setze enabled: process.env.NODE_ENV !== 'production', damit die DevTools-Anbindung im Production-Build vollständig entfernt wird. Tree-Shaking eliminiert dann den gesamten DevTools-Overhead.

immer 3. immer Middleware – Mutations statt Spreads

Motivation

Das Problem mit tief verschachteltem State

Sobald dein Store verschachtelte Objekte enthält, wird immutables Updaten mit dem Spread-Operator mühsam und fehleranfällig. Statt { ...state, user: { ...state.user, address: { ...state.user.address, zip: '10115' }}} schreibst du mit Immer einfach: state.user.address.zip = '10115'.

Die immer-Middleware von Zustand integriert Immer.js direkt in den set-Aufruf. Claude Code erkennt verschachtelte State-Strukturen und schlägt die Immer-Migration automatisch vor.

Setup

immer installieren und konfigurieren

bash npm install immer
TypeScript import { create } from 'zustand'; import { immer } from 'zustand/middleware/immer'; interface ProfileState { user: { name: string; address: { street: string; city: string; zip: string; }; tags: string[]; }; updateCity: (city: string) => void; addTag: (tag: string) => void; removeTag: (tag: string) => void; } export const useProfileStore = create<ProfileState>()( immer((set) => ({ user: { name: 'Max Mustermann', address: { street: 'Hauptstr. 1', city: 'Berlin', zip: '10115' }, tags: ['admin'], }, // Direkte Mutation – Immer erzeugt unveränderliche Kopie updateCity: (city) => set((state) => { state.user.address.city = city; // kein Spread! }), addTag: (tag) => set((state) => { if (!state.user.tags.includes(tag)) { state.user.tags.push(tag); // Array.push – in Immer OK! } }), removeTag: (tag) => set((state) => { const idx = state.user.tags.indexOf(tag); if (idx >= 0) state.user.tags.splice(idx, 1); }), }) );
Immer-Regel: In einem Immer-Producer darfst du entweder mutieren ODER einen neuen Wert zurückgeben – nie beides gleichzeitig. Claude Code warnt dich automatisch, wenn es dieses Muster in deinem Code erkennt.
Performance-Hinweis: Immer verwendet ES6 Proxies und ist in den meisten Anwendungsfällen schnell genug. Bei extrem heiĂźen Pfaden (z. B. 60fps-Animationen mit State-Updates) solltest du strukturelles Sharing durch direkten Spread-Operator bevorzugen.

subscribeWithSelector 4. subscribeWithSelector – granulare Store-Subscriptions

Grundprinzip

Warum subscribeWithSelector?

Die Standard-subscribe()-API von Zustand reagiert auf jede State-Änderung. Mit subscribeWithSelector kannst du einen Selector-Funktion angeben und erhältst nur dann einen Callback, wenn sich der selektierte Wert tatsächlich geändert hat – vergleichbar mit useSelector in Redux.

Das ist besonders nĂĽtzlich fĂĽr Seiteneffekte auĂźerhalb von React-Komponenten: Analytics-Tracking, WebSocket-Synchronisation oder lokale Cache-Invalidierung.

Setup & Watcher-Pattern

subscribeWithSelector aktivieren und Watcher implementieren

TypeScript import { create } from 'zustand'; import { subscribeWithSelector } from 'zustand/middleware'; interface SessionState { userId: string | null; plan: 'free' | 'pro' | 'enterprise'; featureFlags: Record<string, boolean>; setUser: (id: string) => void; setPlan: (plan: SessionState['plan']) => void; } export const useSessionStore = create<SessionState>()( subscribeWithSelector((set) => ({ userId: null, plan: 'free', featureFlags: {}, setUser: (id) => set({ userId: id }), setPlan: (plan) => set({ plan }), })) ); // ---- Watcher außerhalb von React ---- // Nur triggern wenn sich userId ändert const unsubUser = useSessionStore.subscribe( (state) => state.userId, // Selector (userId, prevUserId) => { if (userId && !prevUserId) { analytics.identify(userId); // Login-Event } if (!userId && prevUserId) { analytics.reset(); // Logout-Event } } ); // Plan-Upgrade erkennen const unsubPlan = useSessionStore.subscribe( (state) => state.plan, (plan) => { if (plan === 'pro' || plan === 'enterprise') { loadPremiumFeatures(); } }, { equalityFn: (a, b) => a === b } // Custom Equality ); // Cleanup (z.B. bei App-Teardown) unsubUser(); unsubPlan();
React-Integration

subscribeWithSelector in Komponenten nutzen

Innerhalb von Komponenten bleibt useSessionStore(selector) der bevorzugte Weg. Die .subscribe()-API ist fĂĽr Seiteneffekte auĂźerhalb des React-Lifecycles gedacht. Claude Code macht diesen Unterschied explizit und kommentiert es entsprechend.

TypeScript · React import { useEffect } from 'react'; import { shallow } from 'zustand/shallow'; function PlanBadge() { // In Komponenten: useStore(selector) → kein .subscribe() nötig const { plan, userId } = useSessionStore( (s) => ({ plan: s.plan, userId: s.userId }), shallow ); return <span>{plan}</span>; }

combine 5. combine – mehrere Stores zusammenführen

Grundprinzip

Was macht combine?

combine ist ein Helfer (kein vollwertiges Middleware), der Initial-State und Creator-Funktion zusammenführt und dabei automatisch TypeScript-Typen ableitet. Du musst das Interface nicht mehr manuell definieren – TypeScript inferiert es aus dem initialen State-Objekt.

Automatische Typen

combine mit TypeScript-Inferenz

TypeScript import { create } from 'zustand'; import { combine } from 'zustand/middleware'; // Kein Interface nötig – TypeScript inferiert alle Typen! export const useCounterStore = create( combine( { count: 0, step: 1, history: [] as number[], }, (set, get) => ({ increment: () => set((s) => ({ count: s.count + s.step, history: [...s.history, s.count], })), decrement: () => set((s) => ({ count: s.count - s.step, history: [...s.history, s.count], })), setStep: (step: number) => set({ step }), reset: () => set({ count: 0, history: [] }), // get() liefert aktuellen State ohne Subscription getSnapshot: () => get().count, }) ) ); // TypeScript weiß automatisch: count ist number, increment ist () => void const { count, increment } = useCounterStore.getState();
Wann combine nutzen? Bei einfachen Stores ohne komplexe Interface-Hierarchien. Für komplexe Domain-Stores mit Generics oder expliziten Return-Types bleibt das manuelle Interface-Pattern übersichtlicher. Claude Code wählt das passende Pattern automatisch nach Store-Komplexität.

Chains 6. Middleware kombinieren – Reihenfolge und TypeScript

Reihenfolge

Die richtige Middleware-Reihenfolge

Wenn du mehrere Middlewares kombinierst, ist die Reihenfolge entscheidend. Zustand verarbeitet Middlewares von auĂźen nach innen. Folgende Reihenfolge hat sich als Best Practice etabliert:

Position Middleware BegrĂĽndung
1 (außen) devtools Muss alle State-Änderungen sehen – muss außen sein
2 persist Persistiert nach devtools, vor Immer-Mutations
3 subscribeWithSelector Subscriptions greifen auf persistierten State zu
4 (innen) immer Konvertiert Mutations in immutable Updates
Full Stack

Alle Middlewares kombiniert – Production-Setup

TypeScript import { create } from 'zustand'; import { devtools, persist, subscribeWithSelector, createJSONStorage } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; interface AppState { user: { id: string | null; name: string; plan: 'free' | 'pro' }; ui: { theme: string; sidebarOpen: boolean }; setTheme: (theme: string) => void; login: (id: string, name: string) => void; logout: () => void; } export const useAppStore = create<AppState>()( devtools( // 1. auĂźen persist( // 2. subscribeWithSelector( // 3. immer((set) => ({ // 4. innen user: { id: null, name: '', plan: 'free' }, ui: { theme: 'dark', sidebarOpen: false }, setTheme: (theme) => set((s) => { s.ui.theme = theme; }, false, 'ui/setTheme'), login: (id, name) => set((s) => { s.user.id = id; s.user.name = name; }, false, 'user/login'), logout: () => set((s) => { s.user.id = null; s.user.name = ''; }, false, 'user/logout'), })) ), { name: 'app-store-v1', storage: createJSONStorage(() => localStorage), partialize: (s) => ({ user: s.user, ui: { theme: s.ui.theme }, // sidebarOpen NICHT persistieren }), } ), { name: 'AppStore', enabled: process.env.NODE_ENV !== 'production' } ) ); // Watcher registrieren useAppStore.subscribe( (s) => s.user.id, (id) => id && loadUserDashboard(id) );
TypeScript

TypeScript-Typen beim Kombinieren

Bei kombinierten Middlewares kann TypeScript manchmal Schwierigkeiten mit der Typ-Inferenz haben. Claude Code kennt das Muster und schreibt automatisch den korrekten Generic-Aufruf:

TypeScript // Explizites Interface + Generic bei Middleware-Chains const useStore = create<MyState>()( devtools( persist( immer<MyState>((set) => ({ /* ... */ })), { name: 'my-store' } ), { name: 'MyStore' } ) ); // TypeScript 5.x: satisfies fĂĽr strengere Checks const initialState = { count: 0, name: '', } satisfies Partial<MyState>;
Claude Code Workflow-Tipp: Beschreibe Claude Code deinen Store in natĂĽrlicher Sprache: "Ich brauche einen User-Store mit persist (nur user.id und user.plan), devtools im Dev-Modus, Immer fĂĽr Mutations und einen Plan-Upgrade-Watcher." Claude Code generiert die gesamte Middleware-Chain inklusive TypeScript-Typen und Watcher-Setup in einem Schritt.

Quick Reference – alle Middlewares auf einen Blick

Middleware Import-Pfad Hauptzweck Install
persist zustand/middleware localStorage / sessionStorage Sync –
devtools zustand/middleware Redux DevTools, Time-Travel –
immer zustand/middleware/immer Mutation-basierte Updates npm i immer
subscribeWithSelector zustand/middleware Granulare Subscriptions –
combine zustand/middleware Typ-Inferenz ohne Interface –

Zustand-Middleware mit KI-UnterstĂĽtzung meistern

Starte deinen kostenlosen Trial und lass Claude Code deine Zustand-Stores mit der passenden Middleware-Chain generieren – von persist bis Immer, typsicher und produktionsfertig.

Kostenlos starten →