Rule Writing

CLAUDE.md for Redux and Zustand

AI generates Redux boilerplate from 2018 or misuses Zustand as Redux. Rules for RTK slices, Zustand stores, server state separation, and choosing the right tool.

7 min read·January 30, 2026

AI generates 2018 Redux or misuses Zustand as Redux — pick one, use it right

RTK slices, Zustand stores, server/client state separation, and when to use each

Why State Management Needs Clear Rules

React state management is the most fragmented ecosystem in frontend development. AI assistants generate random combinations: legacy Redux with switch statements, Zustand stores that look like Redux reducers, Context API for frequently changing state (re-renders everything), and server state (API data) mixed with client state (UI state) in one store. The result is state management that's overcomplicated, under-performant, and inconsistent.

The core problem: AI doesn't distinguish between server state (data from APIs — cacheable, shareable, stale-able) and client state (UI state — local, ephemeral, synchronous). Server state belongs in TanStack Query or SWR. Client state belongs in Zustand or Redux. Mixing them in one store is the #1 state management mistake.

These rules cover two tools: Redux Toolkit (RTK) for large, complex applications, and Zustand for most other cases. Specify which your project uses — the patterns are completely different.

Rule 1: Redux Toolkit — Never Legacy Redux

The rule: 'If using Redux, use Redux Toolkit exclusively. Create slices with createSlice: const userSlice = createSlice({ name: "user", initialState, reducers: { setUser: (state, action) => { state.user = action.payload }, logout: (state) => { state.user = null } } }). Never write: switch statements in reducers, manual action creators, manual action types, combineReducers with hand-written reducers, or connect() HOC (use hooks).'

For async: 'Use createAsyncThunk for async operations: const fetchUser = createAsyncThunk("user/fetch", async (id) => { const response = await api.getUser(id); return response.data; }). Handle in extraReducers: builder.addCase(fetchUser.fulfilled, (state, action) => { state.user = action.payload }). Or use RTK Query for API data — it eliminates createAsyncThunk for most use cases.'

AI generates legacy Redux: ACTION_TYPES constants, switch/case reducers, mapStateToProps with connect(). RTK reduces this 80% — one createSlice replaces action types + action creators + reducer. Your rule ensures the AI generates 2024 Redux, not 2018 Redux.

  • createSlice: name + initialState + reducers — replaces 3 files of boilerplate
  • Immer built-in: state.user = action.payload mutates safely (produces new object)
  • createAsyncThunk for async — or RTK Query for API data (even better)
  • useSelector + useDispatch hooks — never connect() HOC
  • Never: switch reducers, ACTION_TYPE constants, mapStateToProps
⚠️ Never Legacy Redux

AI generates ACTION_TYPES, switch/case reducers, and connect(). Redux Toolkit's createSlice replaces all three with one function call. If your Redux code has switch statements, it's from 2018.

Rule 2: Zustand — Minimal Stores, Not Mini-Redux

The rule: 'Use Zustand for simple, focused state. Create stores with create: const useAuthStore = create<AuthState>((set) => ({ user: null, login: async (credentials) => { const user = await api.login(credentials); set({ user }); }, logout: () => set({ user: null }) })). Stores are hooks — use directly in components: const user = useAuthStore(state => state.user). No providers, no boilerplate, no actions/reducers split.'

For selectors: 'Always use selectors to prevent unnecessary re-renders: const name = useAuthStore(state => state.user?.name). Without a selector, the component re-renders on any store change — even unrelated fields. Use shallow comparison for object selectors: useAuthStore(state => ({ name: state.user?.name, email: state.user?.email }), shallow).'

AI generates Zustand stores that look like Redux: separate actions objects, dispatch patterns, and reducer-like logic. Zustand is simpler: state + functions in one object. set() is the only API. No reducers, no dispatch, no action types. If your Zustand store looks like Redux, you should be using Redux.

💡 Always Use Selectors

useAuthStore(state => state.user?.name) re-renders only when name changes. useAuthStore() (no selector) re-renders on ANY store change. One argument prevents every unnecessary re-render in your Zustand app.

Rule 3: Separate Server State from Client State

The rule: 'Server state (API data) goes in TanStack Query — not Redux, not Zustand. Client state (UI state, form state, preferences) goes in Zustand or Redux. Never put API response data in a global store — TanStack Query handles caching, invalidation, refetching, and loading states. The store handles: auth state, UI preferences, sidebar open/closed, form drafts.'

For the separation: 'TanStack Query manages: users list, product catalog, order history, search results — anything from an API. Zustand manages: current theme, sidebar collapsed, selected filters, draft form data — anything local to the UI. These two systems work together: Zustand stores UI preferences, TanStack Query fetches data using those preferences as query keys.'

AI puts everything in one Redux store: API data, loading states, error states, UI state. This creates a maintenance nightmare — every API response requires an action, a reducer case, and manual cache invalidation. TanStack Query handles all of this automatically for server state.

  • TanStack Query: API data, caching, refetch, loading/error states
  • Zustand/Redux: auth state, UI preferences, form drafts, local state
  • Never API data in global store — TanStack Query handles it better
  • Zustand for most apps — Redux only for complex cross-cutting state
  • The two work together: Zustand for preferences, TanStack Query for data
ℹ️ Two Systems, Not One

TanStack Query for server state (API data, caching, refetch). Zustand/Redux for client state (UI, preferences, drafts). They work together. Mixing API data into Redux creates manual cache invalidation nightmares that TanStack Query solves automatically.

Rule 4: Choosing Redux vs Zustand

The rule: 'Use Zustand for: most React applications, simple-to-moderate state needs, projects that value minimal boilerplate, and teams that don't need Redux DevTools' time-travel debugging. Use Redux Toolkit for: large applications with complex cross-cutting state, projects that need middleware (logging, analytics, undo/redo), teams that rely on Redux DevTools, and applications with heavy async orchestration.'

For the decision: 'If your state management code is more boilerplate than logic — use Zustand. If your state management needs middleware, saga-like orchestration, or computed state across multiple slices — use Redux Toolkit. For 80% of React applications, Zustand + TanStack Query is the right answer. Redux is the right answer for the other 20% — large enterprise apps with complex state.'

AI defaults to Redux for everything because Redux dominates training data. Your rule should specify which tool the project uses and prevent the AI from reaching for Redux when Zustand is sufficient (or vice versa).

Rule 5: Persistence and DevTools

The rule: 'For Zustand persistence: use the persist middleware: create(persist((set) => ({ ... }), { name: "auth-storage" })). For Redux persistence: use redux-persist with specific whitelist: persistReducer(config, rootReducer) — never persist the entire store (it includes stale API data). For DevTools: Zustand supports Redux DevTools via the devtools middleware. Redux Toolkit includes DevTools support by default.'

For middleware: 'Zustand middleware composes with create: create(devtools(persist(immer((set) => ({ ... })))). Common middleware: devtools (debugging), persist (localStorage), immer (mutable updates). Keep middleware minimal — add only what you need. Redux middleware: RTK includes thunk by default. Add custom middleware for: logging, analytics tracking, error reporting.'

For testing: 'Test Zustand stores by calling store functions and checking state: useAuthStore.getState().login(creds); expect(useAuthStore.getState().user).toBeDefined(). Test Redux with the store directly: store.dispatch(setUser(user)); expect(store.getState().user.user).toBe(user). Both are testable without rendering components — pure state logic.'

Complete State Management Rules Template

Consolidated rules for Redux Toolkit and/or Zustand projects.

  • Declare your tool: 'This project uses [Zustand/Redux Toolkit]' — AI must know which
  • RTK: createSlice, createAsyncThunk, useSelector/useDispatch — never legacy Redux
  • Zustand: create((set) => state + functions) — selectors to prevent re-renders — never mini-Redux
  • Server state in TanStack Query — client state in Zustand/Redux — never mix
  • Zustand for most apps — Redux for complex cross-cutting state needs
  • persist middleware for localStorage — devtools for debugging — immer for mutations
  • Test stores directly without components — getState() for Zustand, dispatch for Redux
  • shallow comparison for object selectors — specific field selectors over full-state access