| name | zustand |
| description | Expert knowledge for client-side state management with Zustand using the decoupled actions pattern. |
Create Zustand stores following established patterns with proper TypeScript types and middleware.
Quick Start
Copy the template from assets/template.md and replace placeholders:
{{StoreName}} → PascalCase store name (e.g., Project)
{{description}} → Brief description for JSDoc
Core Principles
- Client-Side Only — Use Zustand for client-side state only. Store modules must only be imported from Client Components.
- State Only in Store —
create() should define the state shape and initial values, not actions.
- Decoupled Actions — Export actions as plain functions that update state via
store.setState(...).
- Atomic Selectors — Select the smallest slice of state needed to minimize re-renders.
- useState vs Zustand — Prefer Zustand even for local UI state so components stay dumb and state lives in stores. Use React's
useState only for truly trivial, one-off UI toggles that do not justify a store.
The Decoupled Actions Pattern
Why This Pattern?
- No hook for actions — Components import actions directly to avoid subscribing to action references and causing unnecessary re-renders.
- Testable — Actions are plain functions that can be tested in isolation.
- Tree-shakeable — Unused actions are eliminated from the bundle.
Store Definition
import { create } from "zustand";
interface ExampleState {
count: number;
items: string[];
}
export const useExampleStore = create<ExampleState>(() => ({
count: 0,
items: [],
}));
export const increment = () => {
useExampleStore.setState((state) => ({ count: state.count + 1 }));
};
export const decrement = () => {
useExampleStore.setState((state) => ({ count: state.count - 1 }));
};
export const addItem = (item: string) => {
useExampleStore.setState((state) => ({ items: [...state.items, item] }));
};
export const reset = () => {
useExampleStore.setState({ count: 0, items: [] });
};
Consuming in Client Components
"use client";
import { increment, decrement, useExampleStore } from "@/store/use-example-store";
export function Counter() {
const count = useExampleStore((state) => state.count);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
Best Practices
Default to Stores, Even for Local State
- Store form and UI state in Zustand by default, even when used in a single component, to keep components presentational.
- Use
useState only when the state is tiny, ephemeral, and unlikely to grow (e.g., a one-off boolean toggle).
- If multiple instances of the same UI can render at once, scope the store per instance (avoid shared singleton state collisions).
Always Use subscribeWithSelector
import { create } from "zustand";
import { subscribeWithSelector } from "zustand/middleware";
export const useMyStore = create<MyStore>()(
subscribeWithSelector((set, get) => ({
})),
);
Separate State and Actions
export interface MyState {
items: Item[];
isLoading: boolean;
}
export interface MyActions {
addItem: (item: Item) => void;
loadItems: () => Promise<void>;
}
export type MyStore = MyState & MyActions;
Use Individual Selectors
const items = useMyStore((state) => state.items);
const { items, isLoading } = useMyStore();
Functional Updates
Use functional updates when the new value depends on previous state:
export const increment = () => {
useExampleStore.setState((state) => ({ count: state.count + 1 }));
};
export const incrementBad = () => {
const current = useExampleStore.getState().count;
useExampleStore.setState({ count: current + 1 });
};
Atomic Selectors
Select only what you need to prevent unnecessary re-renders:
const count = useExampleStore((state) => state.count);
const state = useExampleStore();
Multiple Selectors
When you need multiple values, use separate selectors or shallow equality:
import { useShallow } from "zustand/react/shallow";
const count = useExampleStore((state) => state.count);
const items = useExampleStore((state) => state.items);
const { count, items } = useExampleStore(
useShallow((state) => ({ count: state.count, items: state.items })),
);
Don't Import Stores in Server Components
import { useExampleStore } from "@/store/use-example-store";
export default function Page() {
return <ClientCounter initialCount={0} />;
}
Don't Define Actions Inside create()
export const useStore = create<State & Actions>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
export const useStore = create<State>(() => ({ count: 0 }));
export const increment = () => useStore.setState((s) => ({ count: s.count + 1 }));
Quality Checklist
Before completing any Zustand-related task:
- Verify stores are only imported in Client Components (
'use client')
- Actions are exported as standalone functions, not inside
create()
- Components use atomic selectors to minimize re-renders
- Functional updates are used when new state depends on previous state
- Run
bun run lint to verify no type errors