Comparisons

Zustand vs Redux: AI Rules Comparison

Zustand and Redux are both global state management libraries, but their API surface, boilerplate, and mental models differ dramatically. Each needs specific AI rules to prevent the AI from generating the wrong store patterns.

7 min read·May 3, 2025

dispatch() in Zustand = undefined. set() in Redux = bypasses the reducer. 3-line store vs 30-line setup.

Store creation, state updates, selectors, Provider, async patterns, and copy-paste rule templates

Minimal vs Structured State Management

Zustand is minimal: create a store in one function, use it with a hook, update state directly. const useStore = create((set) => ({ count: 0, increment: () => set((s) => ({ count: s.count + 1 })) })). Usage: const count = useStore((s) => s.count). No providers, no reducers, no action types, no dispatch. The entire store is 3 lines. Zustand's philosophy: state management should be as simple as useState but global.

Redux (with Redux Toolkit) is structured: configure a store with slices, dispatch actions to reducers that update state immutably. const counterSlice = createSlice({ name: 'counter', initialState: { count: 0 }, reducers: { increment: (state) => { state.count += 1; } } }). Usage: const count = useSelector((s) => s.counter.count); dispatch(increment()). Provider wraps the app. Redux Toolkit's philosophy: predictable state changes through a structured action/reducer pattern.

Without state management rules: AI generates Redux action types and dispatch patterns in a Zustand project (dispatch does not exist in Zustand). AI generates Zustand's set() pattern in a Redux project (Zustand's API is incompatible with Redux). AI creates a <Provider> wrapper for Zustand (Zustand does not need one). The state management library determines every state interaction pattern. One rule prevents every state-related generation error.

Store Creation: create() vs configureStore

Zustand store creation: const useStore = create((set, get) => ({ users: [], fetchUsers: async () => { const users = await api.getUsers(); set({ users }); }, addUser: (user) => set((s) => ({ users: [...s.users, user] })) })). The store is: one function call (create), state and actions co-located, async actions are just async functions, and the hook is returned directly. No separate files for actions, reducers, or types. AI rule: 'Zustand: create() returns a hook. State and actions in one object. set() for updates. get() to read current state in actions. No boilerplate files.'

Redux Toolkit store creation: const store = configureStore({ reducer: { counter: counterSlice.reducer, users: usersSlice.reducer } }). Each slice: const usersSlice = createSlice({ name: 'users', initialState: [], reducers: { addUser: (state, action) => { state.push(action.payload); } } }). Async: createAsyncThunk('users/fetch', async () => api.getUsers()). Multiple files: store.ts, counterSlice.ts, usersSlice.ts. AI rule: 'Redux Toolkit: configureStore with slice reducers. createSlice for state + reducers. createAsyncThunk for async. dispatch(action()) to trigger.'

The store creation rule prevents: AI generating createSlice in a Zustand project (Zustand has no slices), generating create() in a Redux project (create is Zustand, not Redux), or mixing both patterns (some state in Zustand, some in Redux — confusing and architecturally unsound). The store creation pattern determines the entire state management architecture.

  • Zustand: create((set) => ({ state, actions })) — one function, one file, returns a hook
  • Redux: configureStore({ reducer }) + createSlice({ reducers }) — structured, multi-file
  • Zustand: state and actions co-located in one object. Redux: actions and reducers in slices
  • Zustand: no Provider needed. Redux: <Provider store={store}> wraps the app
  • AI error: createSlice in Zustand = undefined. create() in Redux = wrong library entirely
💡 3 Lines vs 30 Lines

Zustand: const useStore = create((set) => ({ count: 0, increment: () => set(s => ({ count: s.count + 1 })) })). Done. Redux: configureStore, createSlice, export actions, export reducer, add to store, wrap in Provider. Same counter, 10x more code. Zustand is useState-but-global.

State Updates: set() vs dispatch(action)

Zustand state updates: call set() directly from actions. set({ count: newValue }) replaces state fields. set((state) => ({ count: state.count + 1 })) for derived updates. Updates are: immediate (no action dispatching), direct (call set from anywhere), and partial (only specified fields are updated, others remain). Zustand uses Immer internally by default in the latest versions, so you can mutate draft state safely.

Redux state updates: dispatch an action, the reducer processes it, state is updated immutably. dispatch(increment()) triggers the increment reducer. Redux Toolkit uses Immer internally: state.count += 1 in a reducer is safe (Immer produces an immutable update). The update flow: component calls dispatch(action) → action reaches the reducer → reducer produces new state → components re-render. The indirection (dispatch → action → reducer) is intentional: it enables middleware, logging, and time-travel debugging.

The update rule: 'Zustand: call set() directly from actions or components. No dispatch, no action types, no reducer switch statements.' 'Redux: dispatch(action()) to trigger state changes. Never mutate state outside a reducer. Use createSlice reducers with Immer for safe mutations.' AI generating dispatch() in a Zustand project: function does not exist. AI calling set() directly in a Redux project: bypasses the reducer, state is not tracked. The update mechanism is library-determined.

⚠️ dispatch() Does Not Exist in Zustand

Redux pattern: dispatch(increment()). Zustand: call the action directly — useStore.getState().increment() or from inside the store: set(). AI generating dispatch in a Zustand project: ReferenceError. The update mechanism is library-determined. One rule prevents every state update error.

Selectors and Re-Render Performance

Zustand selectors: the hook accepts a selector function. const count = useStore((s) => s.count). The component re-renders only when count changes (shallow comparison by default). Selector best practice: select the minimum state needed. Bad: const store = useStore() (re-renders on ANY state change). Good: const count = useStore((s) => s.count) (re-renders only when count changes). AI rule: 'Zustand: always use a selector in useStore(). Never useStore() without a selector — it re-renders on every state change.'

Redux selectors: useSelector hook with a selector function. const count = useSelector((s) => s.counter.count). Same principle: select the minimum state. Redux also supports createSelector from reselect for memoized computed values (equivalent to derived state). Multiple useSelector calls in one component: each evaluates independently, the component re-renders if any selector value changes. AI rule: 'Redux: useSelector with specific selectors. createSelector for computed/derived state. Never select the entire state — re-renders on every change.'

The selector rule is identical in principle (select minimum state, avoid full-store subscriptions) but different in syntax. Zustand: useStore(selector). Redux: useSelector(selector). AI generating useSelector in a Zustand project: hook does not exist. AI generating useStore in a Redux project: hook does not exist. The selector hook name is library-determined. One rule about which hook to use prevents every component subscription error.

  • Zustand: useStore((s) => s.count) — selector in the hook, re-renders on count change only
  • Redux: useSelector((s) => s.counter.count) — selector in useSelector, same principle
  • Both: never select entire state — re-renders on every change, defeats optimization
  • Redux: createSelector for memoized derived state. Zustand: derive in the selector function
  • AI error: useSelector in Zustand = undefined. useStore in Redux = wrong library hook
ℹ️ Same Principle, Different Hook Name

Zustand: useStore((s) => s.count). Redux: useSelector((s) => s.counter.count). Same principle: select minimum state, re-render only on change. Different hook name. AI using useSelector in Zustand or useStore in Redux: undefined hook error. The selector hook name IS the rule.

When to Choose Each

Choose Zustand when: you want minimal boilerplate (3-line store vs 30-line Redux setup), your global state is simple (auth state, UI preferences, feature flags), you prefer co-located state and actions (one file per store), your team finds Redux's action/reducer indirection unnecessary for your use case, or you want no Provider wrapper (Zustand stores work outside React components too). Zustand is the default for new React projects in 2026 when global state is needed.

Choose Redux when: you need structured state management with strict patterns (action log, time-travel debugging), your state is complex with many interacting slices (large enterprise applications), you want middleware for cross-cutting concerns (logging, analytics, persistence), your team already uses Redux (migration cost is real), or you need the Redux DevTools for debugging (Zustand supports DevTools too, but Redux integration is more mature). Redux is the established choice for large, complex applications.

For most new React projects: start without global state (useState + context covers most cases). When global state is needed: Zustand for simplicity. If Zustand becomes limiting (complex interactions, extensive middleware needs): migrate to Redux Toolkit. The migration path from Zustand to Redux is straightforward — the state shapes are similar, the update patterns need restructuring into slices and actions.

Ready-to-Use Rule Templates

Zustand CLAUDE.md template: '# State Management (Zustand). Stores: create() in src/stores/. State and actions co-located: create((set, get) => ({ state, actions })). Updates: set({ field: value }) or set((s) => ({ field: s.field + 1 })). Selectors: useStore((s) => s.specificField) — always use a selector, never useStore() bare. No Provider needed. DevTools: import { devtools } from zustand/middleware. Async: regular async functions in the store, call set() with the result. Never Redux patterns (dispatch, createSlice, configureStore, useSelector, Provider).'

Redux Toolkit CLAUDE.md template: '# State Management (Redux Toolkit). Store: configureStore in src/store/. Slices: createSlice in src/store/slices/. Updates: dispatch(actionCreator()) from components. Async: createAsyncThunk for API calls with pending/fulfilled/rejected states. Selectors: useSelector((s) => s.slice.field). Provider: <Provider store={store}> wraps the app in layout. DevTools: automatic with configureStore. Middleware: RTK Query for API caching. Never Zustand patterns (create, set, useStore, bare hook without dispatch).'

The templates draw a clear state management boundary. Zustand: create + set + useStore with selector. Redux: configureStore + createSlice + dispatch + useSelector. The patterns are incompatible — mixing them produces undefined function errors. Copy the template for your library. The negative rules prevent the most common confusion: Redux developers generating dispatch in Zustand, Zustand developers generating set in Redux.

Comparison Summary

Summary of Zustand vs Redux AI rules.

  • Store: Zustand create() (3 lines) vs Redux configureStore + slices (30+ lines setup)
  • Updates: Zustand set() directly vs Redux dispatch(action()) through reducers
  • Selectors: useStore(selector) vs useSelector(selector) — same principle, different hooks
  • Provider: Zustand needs none vs Redux requires <Provider store={store}>
  • Async: Zustand async functions with set() vs Redux createAsyncThunk with pending/fulfilled/rejected
  • Boilerplate: Zustand minimal (one file) vs Redux structured (store + slices + types)
  • 2026 default: Zustand for new simple apps. Redux for complex enterprise state
  • Templates: library-specific hooks and patterns — mixing produces undefined function errors