React & State

Zustand mit Claude Code:
React State Management 2026

Zustand State Management: create, Slices, Middleware (persist, devtools, immer), Selektoren, Async Actions und Vergleich zu Redux/Jotai — einfach und mächtig.

📅 6. Mai 2026 ⏱ 9 min Lesezeit 🏷 React, TypeScript, Zustand
← Zurück zum Blog

Zustand ist 2026 das meistgenutzte State-Management-Tool in React-Projekten — und das aus gutem Grund. Kein Boilerplate wie Redux, keine atomare Komplexität wie Recoil, kein Provider-Wrapper-Chaos. In diesem Guide zeigen wir, wie Claude Code dabei hilft, Zustand-Stores typsicher, modular und produktionsreif zu bauen.

1. Zustand Grundlagen: create, useStore, set & get

Zustand (deutsch für "state") wurde 2019 von Daishi Kato bei Poimandres entwickelt und hat sich zur Standardlösung für React State Management außerhalb der React Context API entwickelt. Die Library kommt in der Bundle-Größe auf unter 1 kB (gzipped) — das ist kleiner als die meisten Icon-Libraries.

Mit Claude Code lässt sich ein typsicherer Zustand-Store in Sekunden scaffolden. Claude versteht die Zustand-API vollständig und schlägt automatisch TypeScript-Interfaces, korrekte Selektoren und passende Middleware-Kombinationen vor.

Installation

# Zustand installieren npm install zustand # TypeScript-Support ist eingebaut — kein @types nötig # Optional: immer für nested state updates npm install immer
Grundlagen

Der minimale Zustand-Store

Ein Store besteht aus State-Daten und Aktionen — alles in einem einzigen create-Call.

// store/counter.ts import { create } from 'zustand' // TypeScript Interface für den Store interface CounterState { count: number step: number increment: () => void decrement: () => void incrementByAmount: (amount: number) => void reset: () => void setStep: (step: number) => void } export const useCounterStore = create<CounterState>()((set, get) => ({ count: 0, step: 1, // set() ersetzt State-Teile (shallow merge) increment: () => set((state) => ({ count: state.count + state.step })), decrement: () => set((state) => ({ count: state.count - state.step })), incrementByAmount: (amount) => set((state) => ({ count: state.count + amount })), reset: () => set({ count: 0 }), // get() liest aktuellen State ohne Subscription setStep: (step) => { const current = get().count console.log(`Step von ${current} auf ${step} gesetzt`) set({ step }) }, }))

Verwendung in React-Komponenten

// components/Counter.tsx import { useCounterStore } from '../store/counter' export function Counter() { // Nur die benötigten State-Teile abonnieren const count = useCounterStore((state) => state.count) const step = useCounterStore((state) => state.step) const { increment, decrement, reset, setStep } = useCounterStore() return ( <div> <p>Count: {count} | Step: {step}</p> <button onClick={increment}>+ {step}</button> <button onClick={decrement}>- {step}</button> <button onClick={() => setStep(5)}>Step = 5</button> <button onClick={reset}>Reset</button> </div> ) }

Claude Code Tipp: Wenn du Claude Code sagst "Erstelle einen Zustand-Store für einen Shopping Cart mit TypeScript", generiert es automatisch das Interface, alle CRUD-Aktionen und passende Selektoren — inklusive Getter-Methoden via get().

TypeScript: Vollständig typisierte Store-Definition

Zustand 5.x (aktuelle Version 2026) unterstützt vollständiges TypeScript-Inference. Der doppelte Funktionsaufruf create<T>()() ist notwendig, damit TypeScript die generischen Typen korrekt ableiten kann — das ist kein Bug, sondern ein bekanntes TypeScript-Limitation-Workaround.

// store/types.ts — Shared Types für den Store export interface Product { id: string name: string price: number quantity: number imageUrl: string } export interface CartState { items: Product[] isOpen: boolean couponCode: string | null // Computed (derived) values als Getter-Funktionen getTotalPrice: () => number getItemCount: () => number // Actions addItem: (product: Product) => void removeItem: (id: string) => void updateQuantity: (id: string, qty: number) => void applyCoupon: (code: string) => Promise<boolean> clearCart: () => void } // store/cart.ts import { create } from 'zustand' import type { CartState, Product } from './types' export const useCartStore = create<CartState>()((set, get) => ({ items: [], isOpen: false, couponCode: null, getTotalPrice: () => { const { items, couponCode } = get() const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0) return couponCode === 'SAVE10' ? total * 0.9 : total }, getItemCount: () => get().items.reduce((sum, item) => sum + item.quantity, 0), addItem: (product) => set((state) => { const existing = state.items.find((i) => i.id === product.id) if (existing) { return { items: state.items.map((i) => i.id === product.id ? { ...i, quantity: i.quantity + 1 } : i ), } } return { items: [...state.items, { ...product, quantity: 1 }] } }), removeItem: (id) => set((state) => ({ items: state.items.filter((i) => i.id !== id), })), updateQuantity: (id, qty) => set((state) => ({ items: qty <= 0 ? state.items.filter((i) => i.id !== id) : state.items.map((i) => i.id === id ? { ...i, quantity: qty } : i), })), applyCoupon: async (code) => { const valid = ['SAVE10', 'WELCOME'].includes(code) set({ couponCode: valid ? code : null }) return valid }, clearCart: () => set({ items: [], couponCode: null }), }))

2. Selektoren & Performance-Optimierung

Eine häufige Falle bei Zustand-Einsteigern: Das ganze Store-Objekt abonnieren, was bei jeder State-Änderung einen Re-render auslöst — auch wenn sich der relevante Wert gar nicht geändert hat. Zustand bietet mehrere Strategien, um dieses Problem zu lösen.

Performance

Warum Selektoren wichtig sind

Ohne Selektor re-rendert eine Komponente bei jeder State-Änderung im Store. Mit präzisen Selektoren re-rendert sie nur, wenn sich der selektierte Wert ändert.

Granulare Selektoren

// ❌ Schlecht: Ganzen Store abonnieren const store = useCartStore() // Re-render bei JEDER Änderung // ✅ Besser: Nur benötigte Felder selektieren const itemCount = useCartStore((state) => state.getItemCount()) const isOpen = useCartStore((state) => state.isOpen) // ✅ Optimal: Stabile Referenzen für Actions const addItem = useCartStore((state) => state.addItem) // Actions sind stabile Referenzen — kein Memoisierungsaufwand nötig

Mehrere Felder mit shallow

// store/user.ts import { create } from 'zustand' import { shallow } from 'zustand/shallow' interface UserState { name: string email: string role: 'admin' | 'user' | 'guest' avatar: string lastLogin: Date | null updateProfile: (data: Partial<Pick<UserState, 'name' | 'email'>>) => void } export const useUserStore = create<UserState>()((set) => ({ name: '', email: '', role: 'guest', avatar: '', lastLogin: null, updateProfile: (data) => set(data), })) // components/UserHeader.tsx function UserHeader() { // shallow verhindert Re-render wenn sich nur andere Felder ändern const { name, email, avatar } = useUserStore( (state) => ({ name: state.name, email: state.email, avatar: state.avatar }), shallow ) return <div>{name} ({email})</div> }

subscribeWithSelector: Reaktiv außerhalb von React

import { subscribeWithSelector } from 'zustand/middleware' const useStore = create<CartState>()( subscribeWithSelector((set, get) => ({ // ... store definition })) ) // Außerhalb von React: auf spezifische State-Änderungen reagieren const unsubscribe = useStore.subscribe( (state) => state.items.length, // Selektor (count, prevCount) => { // Callback if (count > prevCount) { analytics.track('item_added', { count }) } }, { equalityFn: (a, b) => a === b, fireImmediately: false } ) // Cleanup in useEffect useEffect(() => unsubscribe, [])

Achtung: Referenz-Gleichheit bei Objekten und Arrays — Zustand nutzt Object.is für Vergleiche. Arrays werden als neue Referenz erkannt, auch wenn der Inhalt gleich ist. Nutze shallow oder eigene Equality-Funktionen.

3. Middleware: persist & devtools

Middleware in Zustand wrappen den create-Call und erweitern das Store-Verhalten. Die zwei wichtigsten Built-in-Middlewares sind persist (localStorage/sessionStorage) und devtools (Redux DevTools Integration).

Middleware

persist + devtools kombinieren

Middleware wird von innen nach außen geschachtelt. Die Reihenfolge ist relevant: devtools(persist(...)) ist der Standard-Pattern für Production-Stores.

// store/settings.ts — persist + devtools import { create } from 'zustand' import { persist, devtools, createJSONStorage } from 'zustand/middleware' interface SettingsState { theme: 'light' | 'dark' | 'system' language: string notifications: boolean sidebarCollapsed: boolean setTheme: (theme: SettingsState['theme']) => void toggleSidebar: () => void setLanguage: (lang: string) => void } export const useSettingsStore = create<SettingsState>()( devtools( persist( (set) => ({ theme: 'system', language: 'de', notifications: true, sidebarCollapsed: false, setTheme: (theme) => set({ theme }, false, 'settings/setTheme'), toggleSidebar: () => set( (state) => ({ sidebarCollapsed: !state.sidebarCollapsed }), false, 'settings/toggleSidebar' ), setLanguage: (language) => set({ language }, false, 'settings/setLanguage'), }), { name: 'app-settings', // localStorage Key storage: createJSONStorage(() => localStorage), // Nur bestimmte Felder persistieren partialize: (state) => ({ theme: state.theme, language: state.language, notifications: state.notifications, // sidebarCollapsed wird NICHT persistiert }), version: 1, // Migration bei Store-Versions-Upgrades migrate: (persistedState: any, version: number) => { if (version === 0) { // Von Version 0 auf 1: 'dark-mode' boolean → theme string return { ...persistedState, theme: persistedState.darkMode ? 'dark' : 'light', } } return persistedState }, } ), { name: 'SettingsStore' } // DevTools Anzeigename ) )

Custom Storage (sessionStorage, IndexedDB, AsyncStorage)

import { StateStorage } from 'zustand/middleware' // IndexedDB-basierter Storage (z.B. für große Datenmengen) const idbStorage: StateStorage = { getItem: async (name) => { const db = await openDB('zustand-store', 1) return db.get('store', name) ?? null }, setItem: async (name, value) => { const db = await openDB('zustand-store', 1) await db.put('store', value, name) }, removeItem: async (name) => { const db = await openDB('zustand-store', 1) await db.delete('store', name) }, } // Verwendung in persist const useStore = create<MyState>()( persist(storeCreator, { name: 'my-large-store', storage: createJSONStorage(() => idbStorage), }) )

DevTools-Integration: Nach Installation der Redux DevTools Browser-Extension (Chrome/Firefox) siehst du alle Zustand-Aktionen mit dem Namen aus dem dritten set()-Argument. Time-Travel-Debugging, State-Export und Action-Log funktionieren out-of-the-box.

4. immer Middleware: Immutability ohne Schmerz

Tief verschachtelter State in JavaScript ist ein bekanntes Problem. Ohne immer sieht ein einfaches Update schnell so aus: set(s => ({...s, a: {...s.a, b: {...s.a.b, c: newVal}}})). Die immer-Middleware von Zustand löst dieses Problem elegant.

Middleware

immer: Mutierender Stil, immutable Ergebnis

Mit immer kannst du State direkt mutieren — immer erkennt die Änderungen und produziert intern ein neues, immutables State-Objekt via Proxy-Magie.

// store/organization.ts — immer für tiefe Updates import { create } from 'zustand' import { immer } from 'zustand/middleware/immer' interface Department { id: string name: string members: { id: string; name: string; role: string }[] budget: { allocated: number; spent: number } } interface OrgState { departments: Record<string, Department> addMember: (deptId: string, member: { id: string; name: string; role: string }) => void updateBudgetSpent: (deptId: string, amount: number) => void promoteMember: (deptId: string, memberId: string, newRole: string) => void } export const useOrgStore = create<OrgState>()( immer((set) => ({ departments: { engineering: { id: 'engineering', name: 'Engineering', members: [ { id: 'u1', name: 'Ana Müller', role: 'Senior Dev' }, ], budget: { allocated: 120000, spent: 45000 }, }, }, // ✅ Mit immer: direktes Mutieren — kein Spread-Chaos addMember: (deptId, member) => set((state) => { state.departments[deptId].members.push(member) }), updateBudgetSpent: (deptId, amount) => set((state) => { state.departments[deptId].budget.spent += amount }), promoteMember: (deptId, memberId, newRole) => set((state) => { const member = state.departments[deptId].members .find((m) => m.id === memberId) if (member) member.role = newRole }), })) )

Vergleich: Mit und ohne immer

// ❌ Ohne immer — Spread-Hölle bei tiefem State updateBudgetSpent: (deptId, amount) => set((state) => ({ departments: { ...state.departments, [deptId]: { ...state.departments[deptId], budget: { ...state.departments[deptId].budget, spent: state.departments[deptId].budget.spent + amount, }, }, }, })), // ✅ Mit immer — sauber und lesbar updateBudgetSpent: (deptId, amount) => set((state) => { state.departments[deptId].budget.spent += amount }),

Performance: immer nutzt strukturelles Sharing — nur die geänderten Teile des State-Baums werden kopiert. Unveränderliche Äste teilen sich die gleichen Objekt-Referenzen. Das ist in der Praxis oft schneller als manuelle Spread-Operatoren bei großen State-Objekten.

5. Slices Pattern: Große Stores modular aufbauen

Wenn eine App wächst, wird ein einzelner monolithischer Store schwer wartbar. Das Slices-Pattern ist Zustand's Antwort auf Redux Toolkit's createSlice — aber ohne den Boilerplate.

Slices

Slice-First Architecture

Jeder Domain-Bereich (Auth, Cart, UI, Notifications) lebt in einem eigenen Slice. Der Root-Store kombiniert alle Slices zu einem einzigen, reaktiven Store-Objekt.

// store/slices/authSlice.ts import { StateCreator } from 'zustand' export interface AuthSlice { user: { id: string; name: string; email: string } | null isAuthenticated: boolean accessToken: string | null login: (email: string, password: string) => Promise<void> logout: () => void refreshToken: () => Promise<void> } export const createAuthSlice: StateCreator< RootState, // Voller Store-Typ (für Cross-Slice-Zugriff) [], [], AuthSlice > = (set, get) => ({ user: null, isAuthenticated: false, accessToken: null, login: async (email, password) => { const response = await fetch('/api/auth/login', { method: 'POST', body: JSON.stringify({ email, password }), headers: { 'Content-Type': 'application/json' }, }) const data = await response.json() set({ user: data.user, accessToken: data.accessToken, isAuthenticated: true, }) // Cross-Slice: NotificationSlice benachrichtigen get().addNotification({ type: 'success', message: `Willkommen, ${data.user.name}!` }) }, logout: () => { set({ user: null, isAuthenticated: false, accessToken: null }) get().clearCart() // Cross-Slice: Cart leeren beim Logout }, refreshToken: async () => { const response = await fetch('/api/auth/refresh') const { accessToken } = await response.json() set({ accessToken }) }, })
// store/slices/notificationSlice.ts export interface NotificationSlice { notifications: { id: string; type: 'info'|'success'|'error'; message: string }[] addNotification: (n: Omit<NotificationSlice['notifications'][number], 'id'>) => void dismissNotification: (id: string) => void } export const createNotificationSlice: StateCreator<RootState, [], [], NotificationSlice> = (set) => ({ notifications: [], addNotification: (n) => set((state) => ({ notifications: [...state.notifications, { ...n, id: crypto.randomUUID() }], })), dismissNotification: (id) => set((state) => ({ notifications: state.notifications.filter((n) => n.id !== id), })), })
// store/index.ts — Root Store: alle Slices kombinieren import { create } from 'zustand' import { devtools, persist } from 'zustand/middleware' import { createAuthSlice, AuthSlice } from './slices/authSlice' import { createNotificationSlice, NotificationSlice } from './slices/notificationSlice' import { createCartSlice, CartSlice } from './slices/cartSlice' // Root State = Union aller Slices export type RootState = AuthSlice & NotificationSlice & CartSlice export const useStore = create<RootState>()( devtools( persist( (...a) => ({ ...createAuthSlice(...a), ...createNotificationSlice(...a), ...createCartSlice(...a), }), { name: 'app-root-store', // Nur Auth persistieren, nicht Notifications partialize: (state) => ({ user: state.user, accessToken: state.accessToken, isAuthenticated: state.isAuthenticated, items: state.items, }), } ), { name: 'RootStore' } ) ) // Bequeme Slice-spezifische Hooks export const useAuthStore = () => useStore((s) => ({ user: s.user, isAuthenticated: s.isAuthenticated, login: s.login, logout: s.logout, })) export const useNotifications = () => useStore((s) => ({ notifications: s.notifications, addNotification: s.addNotification, dismissNotification: s.dismissNotification, }))

6. Async Actions & Vergleich zu Redux/Jotai/Recoil

Zustand hat keine eingebaute Async-Middleware wie Redux Thunk oder Redux Saga — und das ist Absicht. Async Actions sind einfach normale async-Funktionen im Store, die nach Abschluss set() aufrufen.

Async

Async Actions mit Loading & Error State

Das Pattern: Loading-Flag setzen → Async-Operation → State mit Ergebnis setzen → Error-Handling. Kein Middleware-Setup nötig.

// store/products.ts — vollständiger Async Store import { create } from 'zustand' import { immer } from 'zustand/middleware/immer' interface Product { id: string name: string price: number category: string } interface ProductsState { products: Product[] selectedProduct: Product | null isLoading: boolean error: string | null page: number hasMore: boolean filters: { category: string | null; maxPrice: number | null } fetchProducts: (reset?: boolean) => Promise<void> fetchProduct: (id: string) => Promise<void> setFilter: (filter: Partial<ProductsState['filters']>) => void } export const useProductsStore = create<ProductsState>()( immer((set, get) => ({ products: [], selectedProduct: null, isLoading: false, error: null, page: 1, hasMore: true, filters: { category: null, maxPrice: null }, fetchProducts: async (reset = false) => { const { page, filters } = get() const currentPage = reset ? 1 : page set((state) => { state.isLoading = true state.error = null if (reset) state.products = [] }) try { const params = new URLSearchParams({ page: currentPage.toString(), limit: '20', ...(filters.category ? { category: filters.category } : {}), ...(filters.maxPrice ? { maxPrice: filters.maxPrice.toString() } : {}), }) const res = await fetch(`/api/products?${params}`) if (!res.ok) throw new Error(`HTTP ${res.status}`) const { data, meta } = await res.json() set((state) => { state.products.push(...data) state.page = currentPage + 1 state.hasMore = meta.hasMore state.isLoading = false }) } catch (err) { set((state) => { state.error = err instanceof Error ? err.message : 'Unbekannter Fehler' state.isLoading = false }) } }, fetchProduct: async (id) => { set((state) => { state.isLoading = true; state.error = null }) try { const res = await fetch(`/api/products/${id}`) const product: Product = await res.json() set((state) => { state.selectedProduct = product; state.isLoading = false }) } catch (err) { set((state) => { state.error = err instanceof Error ? err.message : 'Ladefehler' state.isLoading = false }) } }, setFilter: (filter) => set((state) => { Object.assign(state.filters, filter) }), })) )

Vergleich: Zustand vs. Redux Toolkit vs. Jotai vs. Recoil

Die Wahl der State-Management-Library hängt vom Projekt-Kontext ab. Hier ein fairer Vergleich der vier populärsten Optionen im Jahr 2026:

Kriterium Zustand Redux Toolkit Jotai Recoil
Bundle-Größe ~1 kB ~15 kB ~3 kB ~14 kB
Boilerplate Minimal Mittel Sehr gering Mittel
TypeScript Nativ Nativ Nativ Gut
DevTools Redux DevTools Redux DevTools Jotai DevTools Recoil DevTools
Async Actions Nativ (async fn) RTK Query / Thunk Atom Effects Atom Effects
Middleware persist, immer, devtools Umfangreich Begrenzt Begrenzt
React 19 Compat. Vollständig Vollständig Vollständig Eingeschränkt
Lernkurve Sehr flach Mittel Flach Mittel
Claude Code Support Exzellent Sehr gut Gut Gut
Serverless/Edge Ja Eingeschränkt Ja Nein

Wann welche Library?

Entscheidungshelfer

Empfehlungen nach Anwendungsfall

  • Zustand: 90% aller React-Apps — einfach, performant, typsicher
  • Redux Toolkit: Enterprise-Apps mit komplexem Async-Flow, großes Team, strikte Konventionen
  • Jotai: Granulare, atomare State-Updates; Server Components mit React 19; Micro-Frontends
  • Recoil: Abhängigkeitsgraphen zwischen Atoms; wird 2026 weniger aktiv gepflegt

Claude Code Best Practice: Starte immer mit Zustand. Wenn du nach 6 Monaten merkst, dass du RTK Query oder komplexe Middleware-Ketten wirklich brauchst, migriere dann. Premature optimization gilt auch für State-Management-Libraries.

React TypeScript Zustand State Management Claude Code Redux immer Middleware Performance

Zustand-Stores mit Claude Code bauen

Claude Code generiert typsichere Zustand-Stores, Slices, Middleware-Konfigurationen und passende React-Hooks — in Sekunden, produktionsreif.

Kostenlos testen — 14 Tage Trial

Kein Kreditkarte erforderlich · Sofortiger Zugang