Zustand mit Claude Code: State Management ohne Boilerplate 2026

Zustand ist die Antwort auf Redux-Overengineering: minimal, typsicher, kein Provider, kein Action-Creator-Boilerplate. Claude Code kennt alle Zustand-Patterns — von einfachen Stores bis zu Slices mit Middleware, Persistence und DevTools-Integration.

1. Zustand vs Redux — der ehrliche Vergleich

Redux war jahrelang Standard, aber der Boilerplate-Overhead ist real: Actions, Reducers, Selectors, Middleware, Provider-Wrapper — für jeden neuen State-Slice sind dutzende Zeilen nötig. Zustand löst dasselbe Problem mit einem Bruchteil des Codes.

VergleichZustand vs Redux — Funktions-Matrix 2026

Kriterium Zustand Redux Toolkit Jotai
Setup-Code ~5 Zeilen ~40 Zeilen ~3 Zeilen
Bundle-Size ~1.1 KB ~47 KB ~3.3 KB
Provider nötig? Nein Ja (Provider) Optional
Redux DevTools Via Middleware Nativ Via Plugin
TypeScript Exzellent Exzellent Exzellent
Persistence persist() Middleware redux-persist (extra) atomWithStorage
Immer-Integration immer() Middleware Nativ (createSlice) Manuell
Learning Curve Sehr flach Steil (viele Konzepte) Flach
Server-State Manuell RTK Query (extra) Manuell
Empfehlung 2026 Client-State Legacy / große Teams Atomic State
Wann Zustand, wann TanStack Query? Zustand = Client-State (UI-Zustand, User-Preferences, Auth-State). TanStack Query = Server-State (API-Daten, Caching, Refetching). Die meisten Apps brauchen beide. Claude Code kombiniert sie automatisch sinnvoll.
# Claude Code Prompt: "Zeig mir den Unterschied zwischen Redux und Zustand anhand eines Warenkorbs" // ❌ Redux Toolkit — der klassische Boilerplate // cartSlice.ts import { createSlice, PayloadAction } from '@reduxjs/toolkit' interface CartState { items: CartItem[]; total: number } const cartSlice = createSlice({ name: 'cart', initialState: { items: [], total: 0 } as CartState, reducers: { addItem: (state, action: PayloadAction<CartItem>) => { state.items.push(action.payload) state.total += action.payload.price }, removeItem: (state, action: PayloadAction<string>) => { state.items = state.items.filter(i => i.id !== action.payload) state.total = state.items.reduce((sum, i) => sum + i.price, 0) } } }) // + store.ts, Provider in App, useAppSelector, useAppDispatch... // ✅ Zustand — gleiche Funktionalität, 80% weniger Code import { create } from 'zustand' interface CartStore { items: CartItem[] total: number addItem: (item: CartItem) => void removeItem: (id: string) => void } const useCartStore = create<CartStore>()((set, get) => ({ items: [], total: 0, addItem: (item) => set(state => ({ items: [...state.items, item], total: state.total + item.price })), removeItem: (id) => { const items = get().items.filter(i => i.id !== id) set({ items, total: items.reduce((sum, i) => sum + i.price, 0) }) } })) // Verwendung — kein Provider, direkt nutzen: const { items, total, addItem } = useCartStore()

2. Store-Design: create(), TypeScript und Actions

Der Schlüssel zu wartbaren Zustand-Stores liegt im Design: State und Actions im selben Objekt, klare TypeScript-Interfaces, und Actions die genug Logik kapseln um direkt verwendbar zu sein.

StoreVollständiger Store mit TypeScript-Typisierung

# Prompt: "Zustand Store für User-Auth mit TypeScript, Login/Logout, Token-Refresh" // stores/authStore.ts import { create } from 'zustand' import { devtools } from 'zustand/middleware' interface User { id: string email: string name: string role: 'admin' | 'user' | 'guest' } interface AuthState { // State user: User | null token: string | null isLoading: boolean error: string | null isLoggedIn: boolean // Derived — direkt im Store // Actions login: (email: string, password: string) => Promise<void> logout: () => void setUser: (user: User) => void clearError: () => void } export const useAuthStore = create<AuthState>()( devtools( (set, get) => ({ // Initial State user: null, token: null, isLoading: false, error: null, isLoggedIn: false, // Actions mit Business-Logik login: async (email, password) => { set({ isLoading: true, error: null }, false, 'auth/loginStart') try { const { user, token } = await loginApi(email, password) set({ user, token, isLoggedIn: true, isLoading: false }, false, 'auth/loginSuccess') } catch (err) { set({ error: (err as Error).message, isLoading: false }, false, 'auth/loginError') } }, logout: () => { set({ user: null, token: null, isLoggedIn: false }, false, 'auth/logout') }, setUser: (user) => set({ user }, false, 'auth/setUser'), clearError: () => set({ error: null }, false, 'auth/clearError'), }), { name: 'AuthStore' } // DevTools-Label ) ) // Verwendung in Komponenten function LoginForm() { const { login, isLoading, error } = useAuthStore() const isLoggedIn = useAuthStore(state => state.isLoggedIn) // Single-Selector return ( <form onSubmit={(e) => { e.preventDefault(); login(email, password) }}> {error && <ErrorMessage>{error}</ErrorMessage>} <button disabled={isLoading}>{isLoading ? 'Laden...' : 'Login'}</button> </form> ) }
Action-Namen in devtools: Das dritte Argument von set() ist der Action-Name im Redux DevTools. Konvention: 'storeName/actionName'. Claude Code fügt diese automatisch ein wenn devtools aktiviert ist.

Patternget() — Auf aktuellen State in Actions zugreifen

# Prompt: "Zustand Store mit get() für komplexe Actions die auf anderen State-Werten aufbauen" interface ShoppingStore { items: CartItem[] discount: number total: number addItem: (item: CartItem) => void applyDiscount: (code: string) => Promise<boolean> recalcTotal: () => void } const useShoppingStore = create<ShoppingStore>()((set, get) => ({ items: [], discount: 0, total: 0, recalcTotal: () => { // get() gibt IMMER aktuellen State zurück const { items, discount } = get() const subtotal = items.reduce((sum, i) => sum + i.price * i.qty, 0) set({ total: subtotal * (1 - discount / 100) }) }, addItem: (item) => { const existing = get().items.find(i => i.id === item.id) if (existing) { set(state => ({ items: state.items.map(i => i.id === item.id ? {...i, qty: i.qty + 1} : i) })) } else { set(state => ({ items: [...state.items, {...item, qty: 1}] })) } get().recalcTotal() // Andere Action aufrufen via get() }, applyDiscount: async (code) => { const discount = await validateDiscountCode(code) if (discount) { set({ discount }) get().recalcTotal() // Nach Discount Neuberechnung return true } return false } }))

3. Slices-Pattern: Großen Store modular aufteilen

Wenn eine App wächst, wird ein einzelner monolithischer Store unübersichtlich. Das Slices-Pattern teilt ihn in thematische Module auf — die dennoch einen gemeinsamen Store teilen und aufeinander zugreifen können.

SlicescombineStores — Modularer Store mit TypeScript

# Prompt: "Zustand Store in Slices aufteilen: Auth, UI, Cart — alle im selben Store kombiniert" // stores/slices/authSlice.ts import type { StateCreator } from 'zustand' export interface AuthSlice { user: User | null isLoggedIn: boolean login: (email: string, pw: string) => Promise<void> logout: () => void } // StateCreator erhält alle Store-Types — Zugriff auf andere Slices möglich! export const createAuthSlice: StateCreator< AuthSlice & UISlice & CartSlice, // Kompletter Store-Typ [], [], AuthSlice > = (set, get) => ({ user: null, isLoggedIn: false, login: async (email, pw) => { const user = await loginApi(email, pw) set({ user, isLoggedIn: true }) // Zugriff auf anderen Slice nach Login get().loadCart(user.id) // CartSlice-Action! }, logout: () => { set({ user: null, isLoggedIn: false }) get().clearCart() // CartSlice beim Logout leeren } }) // stores/slices/uiSlice.ts export interface UISlice { theme: 'light' | 'dark' sidebarOpen: boolean activeModal: string | null toggleTheme: () => void toggleSidebar: () => void openModal: (name: string) => void closeModal: () => void } export const createUISlice: StateCreator<AuthSlice & UISlice & CartSlice, [], [], UISlice> = (set) => ({ theme: 'light', sidebarOpen: false, activeModal: null, toggleTheme: () => set(s => ({ theme: s.theme === 'light' ? 'dark' : 'light' })), toggleSidebar: () => set(s => ({ sidebarOpen: !s.sidebarOpen })), openModal: (name) => set({ activeModal: name }), closeModal: () => set({ activeModal: null }), }) // stores/useStore.ts — Alles zusammenführen import { create } from 'zustand' import { devtools, persist } from 'zustand/middleware' type AppStore = AuthSlice & UISlice & CartSlice export const useStore = create<AppStore>()( devtools( persist( (...a) => ({ ...createAuthSlice(...a), ...createUISlice(...a), ...createCartSlice(...a), }), { name: 'app-store', // Nur bestimmte State-Teile persistieren partialize: (state) => ({ theme: state.theme, user: state.user }) } ) ) ) // Typisierte Slice-Selektoren für einzelne Bereiche export const useAuth = () => useStore(({ user, isLoggedIn, login, logout }) => ({ user, isLoggedIn, login, logout }) ) export const useUI = () => useStore(({ theme, sidebarOpen, toggleTheme, toggleSidebar }) => ({ theme, sidebarOpen, toggleTheme, toggleSidebar }) )
Slice-Zugriff auf andere Slices: Durch den kombinierten Typ AuthSlice & UISlice & CartSlice in StateCreator kann jeder Slice via get() auf Actions anderer Slices zugreifen — ohne Coupling auf Implementierungsebene.

4. Middleware: persist, devtools und immer

Middlewares sind das mächtigste Feature von Zustand — sie wrappen den Store und erweitern ihn um Persistence, Debugging und komfortable Mutation-Patterns, ohne den Store-Code zu verändern.

persistlocalStorage-Persistence mit partialize

# Prompt: "Zustand Store mit persist Middleware, nur ausgewählte Felder speichern, Custom Storage" import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' interface UserPreferencesStore { theme: 'light' | 'dark' | 'system' language: string notifications: boolean fontSize: number recentSearches: string[] // Nicht persistieren (zu groß) sessionData: object // Nicht persistieren (sensibel) setTheme: (theme: 'light' | 'dark' | 'system') => void setLanguage: (lang: string) => void toggleNotify: () => void addSearch: (term: string) => void } export const usePreferences = create<UserPreferencesStore>()( persist( (set) => ({ theme: 'system', language: 'de', notifications: true, fontSize: 16, recentSearches: [], sessionData: {}, setTheme: (theme) => set({ theme }), setLanguage: (language) => set({ language }), toggleNotify: () => set(s => ({ notifications: !s.notifications })), addSearch: (term) => set(s => ({ recentSearches: [term, ...s.recentSearches.filter(t => t !== term)].slice(0, 10) })), }), { name: 'user-preferences', // localStorage Key storage: createJSONStorage(() => localStorage), // Nur diese Felder persistieren! partialize: (state) => ({ theme: state.theme, language: state.language, notifications: state.notifications, fontSize: state.fontSize, // recentSearches und sessionData werden NICHT gespeichert }), // Migration bei Store-Änderungen version: 2, migrate: (persisted: any, version: number) => { if (version === 0) { // Version 0 → 1: 'darkMode' boolean zu 'theme' string return { ...persisted, theme: persisted.darkMode ? 'dark' : 'light' } } if (version === 1) { // Version 1 → 2: 'system' als neuer Theme-Wert return persisted // Rückwärtskompatibel } return persisted } } ) )

immerImmer.js-Integration für komfortable Mutations

# Prompt: "Zustand mit immer Middleware für tief verschachtelte State-Updates" import { create } from 'zustand' import { immer } from 'zustand/middleware/immer' interface TreeState { nodes: Record<string, { id: string; name: string; children: string[]; expanded: boolean }> toggleNode: (id: string) => void renameNode: (id: string, name: string) => void addChild: (parentId: string, child: { id: string; name: string }) => void } const useTreeStore = create<TreeState>()( immer((set) => ({ nodes: { root: { id: 'root', name: 'Root', children: ['a', 'b'], expanded: true }, a: { id: 'a', name: 'Node A', children: [], expanded: false }, }, // Mit immer: direkte Mutations statt Spread-Orgie! toggleNode: (id) => set(state => { state.nodes[id].expanded = !state.nodes[id].expanded // ✅ Direkte Mutation! }), renameNode: (id, name) => set(state => { state.nodes[id].name = name // ✅ Kein Spread nötig }), addChild: (parentId, child) => set(state => { state.nodes[parentId].children.push(child.id) // ✅ Array push direkt state.nodes[child.id] = { ...child, children: [], expanded: false } }), // Ohne immer wäre toggleNode so: // set(state => ({ nodes: { ...state.nodes, [id]: { ...state.nodes[id], expanded: !state.nodes[id].expanded } } })) // → Mit 3 Ebenen Verschachtelung wird das schnell unleserlich })) ) # Middleware-Stack — Reihenfolge ist wichtig! # devtools( persist( immer( ... ) ) ) → devtools als äußerste Schicht const useComplexStore = create<ComplexState>()( devtools( persist( immer( (set, get) => ({ /* Store-Definition */ }) ), { name: 'complex-store' } ), { name: 'ComplexStore' } ) )
Immer-Middleware vs Immer-Paket direkt: Nutze immer aus zustand/middleware/immer — nicht das immer-Paket direkt im set()-Callback. Die Zustand-Middleware integriert sich korrekt mit dem Proxy-System und anderen Middlewares.

5. Async Actions und Loading States

Zustand braucht keine extra Bibliothek für Async — Actions sind einfach async-Funktionen. Das Pattern für Loading States, Error Handling und Optimistic Updates ist dabei konsistent und einfach verständlich.

AsyncLoading States ohne extra Library

# Prompt: "Zustand Store für API-Calls mit Loading/Error States und Optimistic Updates" interface PostsStore { posts: Post[] loading: boolean error: string | null savingId: string | null // Welcher Post wird gerade gespeichert fetchPosts: () => Promise<void> createPost: (data: CreatePostData) => Promise<Post> updatePost: (id: string, data: Partial<Post>) => Promise<void> deletePost: (id: string) => Promise<void> } export const usePostsStore = create<PostsStore>()((set, get) => ({ posts: [], loading: false, error: null, savingId: null, fetchPosts: async () => { set({ loading: true, error: null }) try { const posts = await fetchPostsApi() set({ posts, loading: false }) } catch (e) { set({ error: (e as Error).message, loading: false }) } }, // Optimistic Update: UI sofort aktualisieren, bei Fehler zurückrollen updatePost: async (id, data) => { const prev = get().posts // Snapshot für Rollback // Sofort UI updaten set(state => ({ savingId: id, posts: state.posts.map(p => p.id === id ? { ...p, ...data } : p) })) try { await updatePostApi(id, data) set({ savingId: null }) } catch (e) { // Rollback bei API-Fehler set({ posts: prev, savingId: null, error: (e as Error).message }) } }, deletePost: async (id) => { const prev = get().posts set(state => ({ posts: state.posts.filter(p => p.id !== id) })) // Optimistic try { await deletePostApi(id) } catch (e) { set({ posts: prev, error: (e as Error).message }) // Rollback } }, createPost: async (data) => { const post = await createPostApi(data) set(state => ({ posts: [post, ...state.posts] })) return post } })) // Verwendung in Komponenten function PostList() { const { posts, loading, error, fetchPosts, savingId } = usePostsStore() useEffect(() => { fetchPosts() }, []) if (loading) return <Spinner /> if (error) return <ErrorMessage>{error}</ErrorMessage> return posts.map(post => ( <PostCard key={post.id} post={post} isSaving={savingId === post.id} /> )) }

PatternMulti-Request Tracking ohne Race-Conditions

# Prompt: "Zustand Store der mehrere parallele Requests trackt und Race-Conditions verhindert" interface DataStore { data: Record<string, unknown> loading: Set<string> // Set statt boolean für Multiple Requests errors: Record<string, string> fetchItem: (key: string) => Promise<void> isLoading: (key: string) => boolean } const useDataStore = create<DataStore>()((set, get) => ({ data: {}, loading: new Set(), errors: {}, isLoading: (key) => get().loading.has(key), fetchItem: async (key) => { if (get().isLoading(key)) return // Deduplication if (get().data[key]) return // Bereits gecacht set(state => ({ loading: new Set([...state.loading, key]) })) try { const result = await fetchItemApi(key) set(state => { const loading = new Set(state.loading) loading.delete(key) return { loading, data: { ...state.data, [key]: result } } }) } catch (e) { set(state => { const loading = new Set(state.loading) loading.delete(key) return { loading, errors: { ...state.errors, [key]: (e as Error).message } } }) } } }))

6. Selektoren und Performance-Optimierung

Jede Zustand-Store-Subscription re-rendert die Komponente sobald irgendetwas im Store sich ändert. Performance-Optimierung bedeutet hier: nur auf relevante State-Teile subscriben und Equality-Checks nutzen.

PerformanceuseShallow für Objekt-Selektoren

# Prompt: "Zustand Performance-Optimierung: useShallow, Fine-grained Subscriptions, Equality" import { useShallow } from 'zustand/react/shallow' // ❌ Schlechte Performance: ganzen Store subscriben function BadComponent() { const store = useStore() // Re-render bei JEDER Store-Änderung! return <div>{store.user?.name}</div> } // ✅ Gut: einzelner primitiver Wert function GoodComponent() { // Re-render NUR wenn user.name sich ändert const userName = useStore(state => state.user?.name) return <div>{userName}</div> } // ✅ Gut: useShallow für mehrere Werte function MultiValueComponent() { // useShallow verhindert Re-render wenn Objekte referenz-gleich aber inhaltlich gleich sind const { theme, language, fontSize } = useStore( useShallow(state => ({ theme: state.theme, language: state.language, fontSize: state.fontSize })) ) return <Settings theme={theme} language={language} fontSize={fontSize} /> } // ✅ Gut: Array mit useShallow function ProductList() { const [items, total] = useCartStore( useShallow(state => [state.items, state.total]) ) return <CartDisplay items={items} total={total} /> } // ✅ Memoized Selector für berechnete Werte import { useMemo } from 'react' function CartSummary() { const items = useCartStore(state => state.items) // Berechnung nur wenn items sich ändert const summary = useMemo(() => ({ count: items.reduce((n, i) => n + i.qty, 0), total: items.reduce((s, i) => s + i.price * i.qty, 0), brands: [...new Set(items.map(i => i.brand))] }), [items]) return <div>{summary.count} Artikel — €{summary.total.toFixed(2)}</div> }

AdvancedsubscribeWithSelector — Außerhalb von React reagieren

# Prompt: "Zustand subscribeWithSelector für nicht-React Code: Sync zu localStorage, WebSocket" import { create } from 'zustand' import { subscribeWithSelector } from 'zustand/middleware' const useAppStore = create<AppState>()( subscribeWithSelector((set) => ({ user: null, theme: 'light', wsState: 'disconnected' as 'connected' | 'disconnected', setWsState: (s: 'connected' | 'disconnected') => set({ wsState: s }) })) ) // Außerhalb von React subscriben — z.B. in einem Service // subscribeWithSelector ermöglicht granulare Subscriptions // 1. Theme-Änderungen → document.body class updaten useAppStore.subscribe( (state) => state.theme, // Selector (theme) => { // Callback (nur wenn theme sich ändert) document.body.classList.toggle('dark', theme === 'dark') }, { fireImmediately: true } // Sofort beim Start ausführen ) // 2. User-Login → WebSocket verbinden/trennen let ws: WebSocket | null = null useAppStore.subscribe( (state) => state.user, (user, prevUser) => { if (user && !prevUser) { ws = new WebSocket(`wss://api.example.com/ws?token=${user.token}`) ws.onopen = () => useAppStore.getState().setWsState('connected') ws.onclose = () => useAppStore.getState().setWsState('disconnected') } else if (!user && prevUser) { ws?.close() ws = null } } ) // 3. Store direkt außerhalb von React lesen // useAppStore.getState() — kein Hook nötig! function apiMiddleware(request: Request) { const { user } = useAppStore.getState() if (user?.token) { request.headers.set('Authorization', `Bearer ${user.token}`) } return request } // 4. Store außerhalb von React updaten (z.B. in einem Service/Worker) useAppStore.setState({ wsState: 'connected' }) // Direkt ohne Hook
getState() und setState() ohne React: Zustand ist nicht auf React angewiesen. store.getState() und store.setState() funktionieren in Services, WebWorker, Node.js und jedem anderen Kontext. Claude Code nutzt das für Architektur-Patterns wie Repository Pattern oder Service Layer.

TestingZustand Stores in Tests isolieren

# Prompt: "Zustand Stores für Unit-Tests zurücksetzen, Mock-State injizieren" import { act, renderHook } from '@testing-library/react' import { useCartStore } from './cartStore' // Store vor jedem Test zurücksetzen beforeEach(() => { act(() => useCartStore.setState({ items: [], total: 0 })) }) test('addItem erhöht total korrekt', () => { const { result } = renderHook(() => useCartStore()) act(() => { result.current.addItem({ id: '1', name: 'Test', price: 9.99, qty: 1 }) }) expect(result.current.items).toHaveLength(1) expect(result.current.total).toBe(9.99) }) // Alternativer Ansatz: createStore statt create für besseres Testing import { createStore } from 'zustand/vanilla' const createCartStore = () => createStore<CartState>()((set) => ({ items: [], total: 0, addItem: (item) => set(s => ({ items: [...s.items, item], total: s.total + item.price })) })) test('isolierter Store pro Test', () => { const store = createCartStore() // Neuer Store-Instance je Test store.getState().addItem({ id: '1', name: 'X', price: 5, qty: 1 }) expect(store.getState().total).toBe(5) })
persist-Middleware in Tests: Die persist-Middleware schreibt in localStorage. In Tests kann das zu Interference zwischen Tests führen. Lösung: localStorage.clear() in beforeEach oder createJSONStorage(() => new MemoryStorage()) verwenden.

Claude Code und Zustand: Workflow in der Praxis

Was Claude Code bei Zustand besonders stark macht ist das systemische Denken: Nicht nur einzelne Stores generieren, sondern die gesamte State-Architektur planen — welcher State gehört in Zustand, welcher in TanStack Query, welcher in React local state?

WorkflowClaude Code State-Architektur-Entscheidungen

# Prompt: "Analysiere diese Komponente und entscheide: Zustand vs TanStack Query vs useState" // Claude Code's State-Decision-Framework: // useState → Lokaler UI-State (Formular, Toggle, lokale Animation) const [isOpen, setIsOpen] = useState(false) const [inputValue, setInputValue] = useState('') // Zustand → Shared Client-State (Auth, UI-Preferences, App-State, Cart) const { user, theme } = useStore() // TanStack Query → Server-State (API-Daten mit Caching + Sync) const { data: posts } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts }) // Die meisten Apps brauchen alle drei: function App() { // useState: Drawer offen/zu const [drawerOpen, setDrawerOpen] = useState(false) // Zustand: Eingeloggter User + Theme const { user, theme } = useStore(useShallow(s => ({ user: s.user, theme: s.theme }))) // TanStack Query: Userdaten vom Server const { data: profile } = useQuery({ queryKey: ['profile', user?.id], queryFn: () => fetchProfile(user!.id), enabled: !!user // Nur wenn User eingeloggt }) }

Zustand-Modul im Claude Code Kurs

Im Claude Code Mastery Kurs lernst du Zustand von Grund auf: Store-Design, Slices-Pattern, alle Middlewares (persist, devtools, immer), Async Actions, Performance-Optimierung und Integration mit TanStack Query — alles mit TypeScript und echten Projekten.

14 Tage kostenlos testen →