8.3 KiB
Zustand v4 AI Coding Assistant Standards
Purpose
These guidelines define how an AI coding assistant should generate, refactor, and reason about Zustand (v4) state management code. They serve as enforceable standards to ensure clarity, consistency, maintainability, and performance across all code suggestions.
⸻
- General Rules • Use TypeScript for all Zustand stores. • All stores must be defined with the create() function from Zustand v4. • State must be immutable; never mutate arrays or objects directly. • Use functional updates with set((state) => ...) whenever referencing existing state. • Never use useStore.getState() inside React render logic.
⸻
- Store Creation Rules
Do:
import { create } from 'zustand';
type CounterStore = { count: number; increment: () => void; reset: () => void; };
export const useCounterStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), reset: () => set({ count: 0 }) }));
Don’t: • Define stores inline within components. • Create multiple stores for related state when a single one suffices. • Nest stores inside hooks or conditional logic.
Naming conventions: • Hook: useStore (e.g., useUserStore, useThemeStore). • File: same as hook (e.g., useUserStore.ts).
⸻
- Store Organization Rules • Each feature (e.g., agent-work-orders, knowledge, settings, etc..) should have its own store file. • Combine complex stores using slices, not nested state. • Use middleware (persist, devtools, immer) only when necessary.
Example structure:
src/features/knowledge/state/ ├── knowledgeStore.ts └── slices/ #If necessary ├── nameSlice.ts #a name that represents the slice if needed
⸻
- Selector and Subscription Rules
Core Principle: Components should subscribe only to the exact slice of state they need.
Do:
const count = useCounterStore((s) => s.count); const increment = useCounterStore((s) => s.increment);
Don’t:
const { count, increment } = useCounterStore(); // ❌ Causes unnecessary re-renders
Additional rules: • Use shallow comparison (shallow) if selecting multiple fields. • Avoid subscribing to derived values that can be computed locally.
⸻
- Middleware and Side Effects
Allowed middleware: persist, devtools, immer, subscribeWithSelector.
Rules: • Never persist volatile or sensitive data (e.g., tokens, temp state). • Configure partialize to persist only essential state. • Guard devtools with environment checks.
Example:
import { create } from 'zustand'; import { persist, devtools } from 'zustand/middleware';
export const useSettingsStore = create( devtools( persist( (set) => ({ theme: 'light', toggleTheme: () => set((s) => ({ theme: s.theme === 'light' ? 'dark' : 'light' })) }), { name: 'settings-store', partialize: (state) => ({ theme: state.theme }) } ) ) );
⸻
- Async Logic Rules • Async actions should be defined inside the store. • Avoid direct useEffect calls that depend on store state.
Do:
fetchData: async () => { const data = await api.getData(); set({ data }); }
Don’t:
useEffect(() => { useStore.getState().fetchData(); // ❌ Side effect in React hook }, []);
⸻
- Anti-Patterns
❌ Anti-Pattern 🚫 Reason Subscribing to full store Causes unnecessary re-renders Inline store creation in component Breaks referential integrity Mutating state directly Zustand expects immutability Business logic inside components Should live in store actions Using store for local-only UI state Clutters global state Multiple independent stores for one domain Increases complexity
⸻
- Testing Rules • Each store must be testable as a pure function. • Tests should verify: initial state, action side effects, and immutability.
Example Jest test:
import { useCounterStore } from '../state/useCounterStore';
test('increment increases count', () => { const { increment, count } = useCounterStore.getState(); increment(); expect(useCounterStore.getState().count).toBe(count + 1); });
⸻
- Documentation Rules • Every store file must include: • Top-level JSDoc summarizing store purpose. • Type definitions for state and actions. • Examples for consumption patterns. • Maintain a STATE_GUIDELINES.md index in the repo root linking all store docs.
⸻
- Enforcement Summary (AI Assistant Logic)
When generating Zustand code: • ALWAYS define stores with create() at module scope. • NEVER create stores inside React components. • ALWAYS use selectors in components. • AVOID getState() in render logic. • PREFER shallow comparison for multiple subscriptions. • LIMIT middleware to proven cases (persist, devtools, immer). • TEST every action in isolation. • DOCUMENT store purpose, shape, and actions.
⸻
Zustand v3 → v4 Summary (for AI Coding Assistants)
Overview
Zustand v4 introduced a few key syntax and type changes focused on improving TypeScript inference, middleware chaining, and internal consistency.
All existing concepts (store creation, selectors, middleware, subscriptions) remain — only the patterns and type structure changed.
Core Concept Changes
-
Curried Store Creation:
create()now expects a curried call form when using generics or middleware.
The previous single-call pattern is deprecated. -
TypeScript Inference Improvements:
v4’s curried syntax provides stronger type inference for complex stores and middleware combinations. -
Stricter Generic Typing:
Functions likeset,get, and the store API have tighter TypeScript types.
Any implicitanyusage or loosely typed middleware will now error until corrected.
Middleware Updates
- Middleware is still supported but must be imported from subpaths (e.g.,
zustand/middleware/immer). - The structure of most built-in middlewares (persist, devtools, immer, subscribeWithSelector) remains identical.
- Chaining multiple middlewares now depends on the curried
createsyntax for correct type inference.
Persistence and Migration
persistbehavior is unchanged functionally, but TypeScript typing for themigratefunction now defines the input state asunknown.
You must assert or narrow this type when using TypeScript.- The
name,version, and other options are unchanged.
Type Adjustments
- The
setfunction now includes areplaceparameter for full state replacement. getandapigenerics are explicitly typed and must align with the store definition.- Custom middleware and typed stores may need to specify generic parameters to avoid inference gaps.
Behavior and API Consistency
- Core APIs like
getState(),setState(), andsubscribe()are still valid. - Hook usage (
useStore(state => state.value)) is identical. - Differences are primarily at compile time (typing), not runtime.
Migration/Usage Implications
For AI agents generating Zustand code:
- Always use the curried
create<Type>()(…)pattern when defining stores. - Always import middleware from
zustand/middleware/.... - Expect
set,get, andapito have stricter typings. - Assume
migratein persistence returnsunknownand must be asserted. - Avoid any v3-style
create<Type>(fn)calls. - Middleware chaining depends on the curried syntax — never use nested functions without it.
Reference Behavior
- Functional concepts are unchanged: stores, actions, and reactivity all behave the same.
- Only the declaration pattern and TypeScript inference system differ.
Summary
| Area | Zustand v3 | Zustand v4 |
|---|---|---|
| Store creation | Single function call | Curried two-step syntax |
| TypeScript inference | Looser | Stronger, middleware-aware |
| Middleware imports | Flat path | Sub-path imports |
| Migrate typing | any |
unknown |
| API methods | Same | Same, stricter typing |
| Runtime behavior | Same | Same |
Key Principle for Code Generation
“If defining a store, always use the curried
create()syntax, import middleware from subpaths, and respect stricter generics. All functional behavior remains identical to v3.”
Recommended Source: Zustand v4 Migration Guide – Official Docs