Zustand Conventions Your AI Agent Will Actually Follow
Zustand is simple but flexible -which means AI agents make different choices every time. Here's how to encode your store conventions.
Zustand flexibility = AI inconsistency
Zustand's minimal API means there are many valid ways to structure stores: one global store vs many slices, using immer vs vanilla setters, selectors inline vs extracted. Without rules, AI agents make different architectural choices in different files -one store is sliced, another is flat, some use immer, some don't.
The key decisions to encode
(1) One store per domain or one global store with slices? (2) Direct mutations with immer or functional setters? (3) How to type the store in TypeScript. (4) When to use Zustand at all -vs useState for local state, vs React Query for server state. (5) Selector patterns to prevent unnecessary re-renders.
Zustand rules template
Add this to your .cursor/rules or CLAUDE.md.
---
description: Zustand state management conventions
globs: ["src/store/**", "**/*.ts", "**/*.tsx"]
alwaysApply: false
---
# Zustand State Management Rules
## When to Use Zustand
✓ Global UI state that is NOT server data: modal open/closed, active tab, user preferences.
✗ Server data (API responses) → use React Query / TanStack Query.
✗ Form state → use react-hook-form.
✗ Local component state → use useState.
## Store Structure
- One store file per domain: src/store/ui-store.ts, src/store/auth-store.ts.
- Export a typed store hook and the store itself.
- TypeScript interface for every store shape:
```typescript
interface UIStore {
isSidebarOpen: boolean
activeModal: 'login' | 'signup' | null
setIsSidebarOpen: (open: boolean) => void
setActiveModal: (modal: UIStore['activeModal']) => void
closeAllModals: () => void
}
export const useUIStore = create<UIStore>((set) => ({
isSidebarOpen: false,
activeModal: null,
setIsSidebarOpen: (open) => set({ isSidebarOpen: open }),
setActiveModal: (modal) => set({ activeModal: modal }),
closeAllModals: () => set({ activeModal: null }),
}))
```
## Selectors
- Use granular selectors to prevent unnecessary re-renders:
✓ const isSidebarOpen = useUIStore((s) => s.isSidebarOpen)
✗ const { isSidebarOpen } = useUIStore() // re-renders on any store change
## Naming
- Store files: src/store/[domain]-store.ts
- Hook exports: useXStore (useUIStore, useAuthStore)
- Boolean state: isX / hasX / canX prefix