checklists/zustand-checklist.md
# Zustand Implementation Checklist
Comprehensive checklist for production-ready Zustand stores.
## Store Setup
### TypeScript Configuration
- [ ] Store interface defined with all state and actions
- [ ] `create<State>()()` double-call pattern used for type inference
- [ ] Action return types are `void` (mutations via `set()`)
- [ ] `type {} from '@redux-devtools/extension'` imported for devtools typing
### Store Structure
- [ ] Single store with slices (not multiple separate stores)
- [ ] Each slice has single responsibility (auth, cart, ui, etc.)
- [ ] Initial state extracted to const for reset functionality
- [ ] Reset action implemented for testing/logout
### Middleware Stack
- [ ] Middleware applied in correct order: `persist(devtools(subscribeWithSelector(immer(...))))`
- [ ] Immer used if nested state updates needed (3+ levels deep)
- [ ] DevTools enabled for development only
- [ ] DevTools has meaningful store name
## Selectors
### Basic Selectors
- [ ] Every state access uses a selector
- [ ] No full-store destructuring: `const { x, y } = useStore()` ❌
- [ ] Selectors are granular (one value per selector when possible)
### Multi-Value Selectors
- [ ] `useShallow` used for selecting multiple related values
- [ ] Import `useShallow` from `zustand/shallow` (the v5 migration-guide path)
- [ ] Deprecated: passing an equality fn to `create()` — migrate to `createWithEqualityFn` from `zustand/traditional`
### Computed Values
- [ ] Derived state computed in selectors, not stored
- [ ] Expensive computations memoized with `useMemo` if needed
### Action Selectors
- [ ] Action selectors exported for stable references
- [ ] Actions grouped by domain: `useAuthActions()`, `useCartActions()`
## Persistence
### Configuration
- [ ] `partialize` used to persist only necessary fields
- [ ] Ephemeral state excluded (loading, errors, UI toggles)
- [ ] Storage key is unique and descriptive
### Migrations
- [ ] `version` field set (start at 1)
- [ ] `migrate` function handles all version transitions
- [ ] Migrations are tested
- [ ] `onRehydrateStorage` handles errors gracefully
### Storage Selection
- [ ] localStorage for cross-tab persistence
- [ ] sessionStorage for tab-scoped persistence
- [ ] IndexedDB for large data (via idb-keyval)
## DevTools
### Configuration
- [ ] DevTools disabled in production: `enabled: process.env.NODE_ENV === 'development'`
- [ ] Store has descriptive name
- [ ] Sensitive data sanitized in serialize config
### Action Naming
- [ ] All `set()` calls include action name: `set(fn, undefined, 'domain/action')`
- [ ] Action names follow convention: `domain/action` or `domain/sub/action`
- [ ] No anonymous actions in devtools timeline
## Performance
### Re-render Prevention
- [ ] Components only subscribe to needed state
- [ ] Large lists use virtualization
- [ ] Expensive selectors memoized
### Bundle Size
- [ ] Tree-shaking works (check bundle analyzer)
- [ ] Unused middleware not imported
## Testing
### Test Setup
- [ ] Store can be reset between tests
- [ ] `getState()` used for assertions
- [ ] `setState()` used for test setup
### Test Coverage
- [ ] All actions tested
- [ ] Selector outputs verified
- [ ] Persistence/rehydration tested
- [ ] Migrations tested with old state snapshots
## Integration
### React Query Separation
- [ ] Server state in React Query (API data)
- [ ] Client state in Zustand (UI, preferences)
- [ ] No API calls in Zustand actions (use React Query mutations)
### SSR/RSC Considerations
- [ ] Hydration mismatch handled
- [ ] `useStore` only called in client components
- [ ] Initial state matches server render
## Code Organization
### File Structure
```
stores/
├── index.ts # Re-exports
├── app-store.ts # Main store with all slices
├── slices/
│ ├── auth-slice.ts
│ ├── cart-slice.ts
│ └── ui-slice.ts
├── selectors/
│ └── index.ts # All selector exports
└── types.ts # Shared types
```
### Naming Conventions
- [ ] Store hook: `useAppStore`, `useAuthStore`
- [ ] Selectors: `useUser`, `useCartItems`, `useTheme`
- [ ] Action selectors: `useAuthActions`, `useCartActions`
- [ ] Slices: `createAuthSlice`, `createCartSlice`
## Security
- [ ] No sensitive data in persisted state (tokens, passwords)
- [ ] DevTools sanitizes sensitive fields
- [ ] Auth tokens stored in memory-only slice or secure storage
## Documentation
- [ ] Store interface documented with JSDoc
- [ ] Complex actions have usage examples
- [ ] Migration history documented
- [ ] README explains store architecture
examples/zustand-examples.md
# Zustand Real-World Examples
Production-tested patterns for common use cases.
## E-Commerce Cart Store
Complete shopping cart with persistence, optimistic updates, and computed totals.
```typescript
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
interface CartItem {
id: string;
productId: string;
name: string;
price: number;
quantity: number;
image: string;
}
interface CartState {
items: CartItem[];
addItem: (item: Omit<CartItem, 'id'>) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
clearCart: () => void;
}
export const useCartStore = create<CartState>()(
persist(
immer((set) => ({
items: [],
addItem: (item) =>
set((state) => {
const existing = state.items.find((i) => i.productId === item.productId);
if (existing) {
existing.quantity += item.quantity;
} else {
state.items.push({ ...item, id: crypto.randomUUID() });
}
}),
removeItem: (id) =>
set((state) => {
state.items = state.items.filter((i) => i.id !== id);
}),
updateQuantity: (id, quantity) =>
set((state) => {
const item = state.items.find((i) => i.id === id);
if (item) {
item.quantity = Math.max(0, quantity);
if (item.quantity === 0) {
state.items = state.items.filter((i) => i.id !== id);
}
}
}),
clearCart: () => set({ items: [] }),
})),
{
name: 'cart-storage',
storage: createJSONStorage(() => localStorage),
}
)
);
// ✅ Computed selectors (not stored state)
export const useCartItemCount = () =>
useCartStore((s) => s.items.reduce((sum, item) => sum + item.quantity, 0));
export const useCartSubtotal = () =>
useCartStore((s) => s.items.reduce((sum, item) => sum + item.price * item.quantity, 0));
export const useCartTax = () => {
const subtotal = useCartSubtotal();
return subtotal * 0.1; // 10% tax
};
export const useCartTotal = () => {
const subtotal = useCartSubtotal();
const tax = useCartTax();
return subtotal + tax;
};
// Usage in component
function CartSummary() {
const itemCount = useCartItemCount();
const subtotal = useCartSubtotal();
const tax = useCartTax();
const total = useCartTotal();
return (
<div>
<p>{itemCount} items</p>
<p>Subtotal: ${subtotal.toFixed(2)}</p>
<p>Tax: ${tax.toFixed(2)}</p>
<p>Total: ${total.toFixed(2)}</p>
</div>
);
}
```
## Authentication Store with Token Refresh
Auth state with automatic token refresh and secure handling.
```typescript
import { create } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';
interface User {
id: string;
email: string;
name: string;
role: 'user' | 'admin';
}
interface AuthState {
user: User | null;
accessToken: string | null;
refreshToken: string | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
refreshAuth: () => Promise<void>;
setUser: (user: User) => void;
}
export const useAuthStore = create<AuthState>()(
subscribeWithSelector((set, get) => ({
user: null,
accessToken: null,
refreshToken: null,
isAuthenticated: false,
isLoading: false,
login: async (email, password) => {
set({ isLoading: true });
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) throw new Error('Login failed');
const { user, accessToken, refreshToken } = await response.json();
set({
user,
accessToken,
refreshToken,
isAuthenticated: true,
isLoading: false,
});
} catch (error) {
set({ isLoading: false });
throw error;
}
},
logout: () => {
set({
user: null,
accessToken: null,
refreshToken: null,
isAuthenticated: false,
});
},
refreshAuth: async () => {
const { refreshToken } = get();
if (!refreshToken) {
get().logout();
return;
}
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
if (!response.ok) {
get().logout();
return;
}
const { accessToken, refreshToken: newRefreshToken } = await response.json();
set({
accessToken,
refreshToken: newRefreshToken,
});
} catch {
get().logout();
}
},
setUser: (user) => set({ user }),
}))
);
// ✅ Auto-refresh token before expiry
if (typeof window !== 'undefined') {
useAuthStore.subscribe(
(state) => state.accessToken,
(accessToken) => {
if (accessToken) {
// Decode JWT to get expiry (simplified)
const payload = JSON.parse(atob(accessToken.split('.')[1]));
const expiresAt = payload.exp * 1000;
const refreshAt = expiresAt - 60000; // Refresh 1 min before expiry
const timeout = setTimeout(() => {
useAuthStore.getState().refreshAuth();
}, refreshAt - Date.now());
return () => clearTimeout(timeout);
}
}
);
}
// ✅ Selectors
export const useUser = () => useAuthStore((s) => s.user);
export const useIsAuthenticated = () => useAuthStore((s) => s.isAuthenticated);
export const useIsAdmin = () => useAuthStore((s) => s.user?.role === 'admin');
export const useAccessToken = () => useAuthStore((s) => s.accessToken);
```
## Theme Store with System Preference Sync
Theme management that syncs with system preferences.
```typescript
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { subscribeWithSelector } from 'zustand/middleware';
type Theme = 'light' | 'dark' | 'system';
type ResolvedTheme = 'light' | 'dark';
interface ThemeState {
theme: Theme;
resolvedTheme: ResolvedTheme;
setTheme: (theme: Theme) => void;
}
const getSystemTheme = (): ResolvedTheme =>
typeof window !== 'undefined' &&
window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
const resolveTheme = (theme: Theme): ResolvedTheme =>
theme === 'system' ? getSystemTheme() : theme;
export const useThemeStore = create<ThemeState>()(
persist(
subscribeWithSelector((set) => ({
theme: 'system',
resolvedTheme: getSystemTheme(),
setTheme: (theme) =>
set({
theme,
resolvedTheme: resolveTheme(theme),
}),
})),
{
name: 'theme-storage',
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({ theme: state.theme }), // Only persist preference
onRehydrateStorage: () => (state) => {
// Resolve theme after hydration
if (state) {
state.resolvedTheme = resolveTheme(state.theme);
}
},
}
)
);
// ✅ Sync with system preference changes
if (typeof window !== 'undefined') {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', () => {
const { theme } = useThemeStore.getState();
if (theme === 'system') {
useThemeStore.setState({ resolvedTheme: getSystemTheme() });
}
});
// Apply theme to document
useThemeStore.subscribe(
(state) => state.resolvedTheme,
(resolvedTheme) => {
document.documentElement.classList.remove('light', 'dark');
document.documentElement.classList.add(resolvedTheme);
},
{ fireImmediately: true }
);
}
// ✅ Selectors
export const useTheme = () => useThemeStore((s) => s.theme);
export const useResolvedTheme = () => useThemeStore((s) => s.resolvedTheme);
export const useIsDarkMode = () => useThemeStore((s) => s.resolvedTheme === 'dark');
```
## Multi-Step Form Wizard Store
Form wizard with step validation and draft persistence.
```typescript
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
interface PersonalInfo {
firstName: string;
lastName: string;
email: string;
}
interface AddressInfo {
street: string;
city: string;
state: string;
zip: string;
}
interface PaymentInfo {
cardNumber: string;
expiryDate: string;
cvv: string;
}
interface WizardState {
currentStep: number;
personal: Partial<PersonalInfo>;
address: Partial<AddressInfo>;
payment: Partial<PaymentInfo>;
completedSteps: Set<number>;
setStep: (step: number) => void;
nextStep: () => void;
prevStep: () => void;
updatePersonal: (data: Partial<PersonalInfo>) => void;
updateAddress: (data: Partial<AddressInfo>) => void;
updatePayment: (data: Partial<PaymentInfo>) => void;
markStepComplete: (step: number) => void;
reset: () => void;
}
const TOTAL_STEPS = 3;
const initialState = {
currentStep: 0,
personal: {},
address: {},
payment: {},
completedSteps: new Set<number>(),
};
export const useWizardStore = create<WizardState>()(
persist(
immer((set) => ({
...initialState,
setStep: (step) =>
set((state) => {
if (step >= 0 && step < TOTAL_STEPS) {
state.currentStep = step;
}
}),
nextStep: () =>
set((state) => {
if (state.currentStep < TOTAL_STEPS - 1) {
state.currentStep += 1;
}
}),
prevStep: () =>
set((state) => {
if (state.currentStep > 0) {
state.currentStep -= 1;
}
}),
updatePersonal: (data) =>
set((state) => {
Object.assign(state.personal, data);
}),
updateAddress: (data) =>
set((state) => {
Object.assign(state.address, data);
}),
updatePayment: (data) =>
set((state) => {
Object.assign(state.payment, data);
}),
markStepComplete: (step) =>
set((state) => {
state.completedSteps.add(step);
}),
reset: () => set(initialState),
})),
{
name: 'wizard-draft',
storage: createJSONStorage(() => sessionStorage), // Tab-scoped
partialize: (state) => ({
currentStep: state.currentStep,
personal: state.personal,
address: state.address,
// Don't persist payment info for security
}),
}
)
);
// ✅ Selectors
export const useCurrentStep = () => useWizardStore((s) => s.currentStep);
export const useIsFirstStep = () => useWizardStore((s) => s.currentStep === 0);
export const useIsLastStep = () => useWizardStore((s) => s.currentStep === TOTAL_STEPS - 1);
export const useWizardProgress = () => useWizardStore((s) => ((s.currentStep + 1) / TOTAL_STEPS) * 100);
```
## Notification Toast Store
Global notification system with auto-dismiss.
```typescript
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
type NotificationType = 'info' | 'success' | 'warning' | 'error';
interface Notification {
id: string;
type: NotificationType;
title: string;
message?: string;
duration?: number;
dismissible?: boolean;
}
interface NotificationState {
notifications: Notification[];
add: (notification: Omit<Notification, 'id'>) => string;
remove: (id: string) => void;
clear: () => void;
}
const DEFAULT_DURATION = 5000;
export const useNotificationStore = create<NotificationState>()(
immer((set, get) => ({
notifications: [],
add: (notification) => {
const id = crypto.randomUUID();
const duration = notification.duration ?? DEFAULT_DURATION;
set((state) => {
state.notifications.push({
...notification,
id,
dismissible: notification.dismissible ?? true,
});
});
// Auto-dismiss after duration
if (duration > 0) {
setTimeout(() => {
get().remove(id);
}, duration);
}
return id;
},
remove: (id) =>
set((state) => {
state.notifications = state.notifications.filter((n) => n.id !== id);
}),
clear: () => set({ notifications: [] }),
}))
);
// ✅ Convenience functions
export const toast = {
info: (title: string, message?: string) =>
useNotificationStore.getState().add({ type: 'info', title, message }),
success: (title: string, message?: string) =>
useNotificationStore.getState().add({ type: 'success', title, message }),
warning: (title: string, message?: string) =>
useNotificationStore.getState().add({ type: 'warning', title, message }),
error: (title: string, message?: string) =>
useNotificationStore.getState().add({ type: 'error', title, message, duration: 0 }),
};
// ✅ Selectors
export const useNotifications = () => useNotificationStore((s) => s.notifications);
export const useHasNotifications = () => useNotificationStore((s) => s.notifications.length > 0);
```
references/anti-patterns-and-integration.md
# Zustand Anti-Patterns & React Query Integration
## Anti-Patterns (FORBIDDEN)
```typescript
// FORBIDDEN: Destructuring entire store
const { count, increment } = useStore(); // Re-renders on ANY state change
// FORBIDDEN: Storing derived/computed state
const useStore = create((set) => ({
items: [],
total: 0, // Will get out of sync!
}));
// FORBIDDEN: Storing server state
const useStore = create((set) => ({
users: [], // Use TanStack Query instead
fetchUsers: async () => { ... },
}));
// FORBIDDEN: Mutating state without Immer
set((state) => {
state.items.push(item); // Breaks reactivity!
return state;
});
// FORBIDDEN: Using deprecated shallow import
import { shallow } from 'zustand/shallow'; // Use useShallow from zustand/react/shallow
```
## Integration with React Query
```typescript
// Zustand for CLIENT state (UI, preferences, local-only)
const useUIStore = create<UIState>()((set) => ({
sidebarOpen: false,
theme: 'light',
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
}));
// TanStack Query for SERVER state (API data)
function Dashboard() {
const sidebarOpen = useUIStore((s) => s.sidebarOpen);
const { data: users } = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
// Zustand: UI state | TanStack Query: server data
}
```
references/core-patterns.md
# Zustand Core Patterns
## 1. Basic Store with TypeScript
```typescript
import { create } from 'zustand';
interface BearState {
bears: number;
increase: (by: number) => void;
reset: () => void;
}
const useBearStore = create<BearState>()((set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
reset: () => set({ bears: 0 }),
}));
```
## 2. Slices Pattern (Modular Stores)
```typescript
import { create, StateCreator } from 'zustand';
// Auth slice
interface AuthSlice {
user: User | null;
login: (user: User) => void;
logout: () => void;
}
const createAuthSlice: StateCreator<AuthSlice & CartSlice, [], [], AuthSlice> = (set) => ({
user: null,
login: (user) => set({ user }),
logout: () => set({ user: null }),
});
// Cart slice
interface CartSlice {
items: CartItem[];
addItem: (item: CartItem) => void;
clearCart: () => void;
}
const createCartSlice: StateCreator<AuthSlice & CartSlice, [], [], CartSlice> = (set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
clearCart: () => set({ items: [] }),
});
// Combined store
const useStore = create<AuthSlice & CartSlice>()((...a) => ({
...createAuthSlice(...a),
...createCartSlice(...a),
}));
```
## 3. Immer Middleware (Immutable Updates)
```typescript
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
interface TodoState {
todos: Todo[];
addTodo: (text: string) => void;
toggleTodo: (id: string) => void;
updateNested: (id: string, subtaskId: string, done: boolean) => void;
}
const useTodoStore = create<TodoState>()(
immer((set) => ({
todos: [],
addTodo: (text) =>
set((state) => {
state.todos.push({ id: crypto.randomUUID(), text, done: false });
}),
toggleTodo: (id) =>
set((state) => {
const todo = state.todos.find((t) => t.id === id);
if (todo) todo.done = !todo.done;
}),
updateNested: (id, subtaskId, done) =>
set((state) => {
const todo = state.todos.find((t) => t.id === id);
const subtask = todo?.subtasks?.find((s) => s.id === subtaskId);
if (subtask) subtask.done = done;
}),
}))
);
```
## 4. Persist Middleware
```typescript
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
interface SettingsState {
theme: 'light' | 'dark';
language: string;
setTheme: (theme: 'light' | 'dark') => void;
}
const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
theme: 'light',
language: 'en',
setTheme: (theme) => set({ theme }),
}),
{
name: 'settings-storage',
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({ theme: state.theme }), // Only persist theme
version: 1,
migrate: (persisted, version) => {
if (version === 0) {
// Migration logic
}
return persisted as SettingsState;
},
}
)
);
```
## 5. Selectors (Prevent Re-renders)
```typescript
// BAD: Re-renders on ANY state change
const { bears, fish } = useBearStore();
// GOOD: Only re-renders when bears changes
const bears = useBearStore((state) => state.bears);
// GOOD: Shallow comparison for objects (Zustand 5.x)
import { useShallow } from 'zustand/react/shallow';
const { bears, fish } = useBearStore(
useShallow((state) => ({ bears: state.bears, fish: state.fish }))
);
// GOOD: Computed/derived state via selector
const totalAnimals = useBearStore((state) => state.bears + state.fish);
// BAD: Storing computed state
const useStore = create((set) => ({
items: [],
total: 0, // Don't store derived values!
addItem: (item) => set((s) => ({
items: [...s.items, item],
total: s.total + item.price, // Sync issues!
})),
}));
// GOOD: Compute in selector
const total = useStore((s) => s.items.reduce((sum, i) => sum + i.price, 0));
```
## 6. Async Actions
```typescript
interface UserState {
user: User | null;
loading: boolean;
error: string | null;
fetchUser: (id: string) => Promise<void>;
}
const useUserStore = create<UserState>()((set) => ({
user: null,
loading: false,
error: null,
fetchUser: async (id) => {
set({ loading: true, error: null });
try {
const user = await api.getUser(id);
set({ user, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
}));
```
## 7. DevTools Integration
```typescript
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
const useStore = create<State>()(
devtools(
(set) => ({
// ... state and actions
}),
{ name: 'MyStore', enabled: process.env.NODE_ENV === 'development' }
)
);
```
references/middleware-composition.md
# Middleware Composition
Comprehensive guide to combining Zustand middleware in the correct order for production applications.
## Middleware Execution Order
Middleware wraps from **inside out**. The innermost middleware executes first, outermost last.
```
┌─────────────────────────────────────────────────────────────┐
│ persist (outermost - serializes final state) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ devtools (records actions after transformation) │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ subscribeWithSelector (enables granular subs) │ │ │
│ │ │ ┌───────────────────────────────────────────┐ │ │ │
│ │ │ │ immer (innermost - transforms mutations) │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ Your store logic lives here │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ └───────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## Why Order Matters
| Position | Middleware | Reason |
|----------|------------|--------|
| Innermost | `immer` | Transforms draft mutations into immutable updates FIRST |
| Middle | `subscribeWithSelector` | Needs transformed (immutable) state to work correctly |
| Middle | `devtools` | Records actions AFTER immer transforms them |
| Outermost | `persist` | Serializes the FINAL transformed state |
## Complete Production Setup
```typescript
import { create } from 'zustand';
import { devtools, persist, subscribeWithSelector, createJSONStorage } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import type {} from '@redux-devtools/extension'; // Required for devtools typing
interface AppState {
// UI State
sidebarOpen: boolean;
theme: 'light' | 'dark' | 'system';
// User preferences
notifications: {
email: boolean;
push: boolean;
sms: boolean;
};
// Actions
toggleSidebar: () => void;
setTheme: (theme: 'light' | 'dark' | 'system') => void;
updateNotification: (key: keyof AppState['notifications'], value: boolean) => void;
reset: () => void;
}
const initialState = {
sidebarOpen: true,
theme: 'system' as const,
notifications: {
email: true,
push: true,
sms: false,
},
};
export const useAppStore = create<AppState>()(
persist(
devtools(
subscribeWithSelector(
immer((set, get) => ({
...initialState,
toggleSidebar: () =>
set(
(state) => { state.sidebarOpen = !state.sidebarOpen; },
undefined,
'ui/toggleSidebar' // Action name for devtools
),
setTheme: (theme) =>
set(
(state) => { state.theme = theme; },
undefined,
'ui/setTheme'
),
updateNotification: (key, value) =>
set(
(state) => { state.notifications[key] = value; },
undefined,
`notifications/update/${key}`
),
reset: () =>
set(
() => initialState,
true, // Replace entire state
'app/reset'
),
}))
),
{
name: 'AppStore',
enabled: process.env.NODE_ENV === 'development',
// Sanitize sensitive data from devtools
serialize: {
replacer: (key, value) => {
if (key === 'password' || key === 'token') return '[REDACTED]';
return value;
},
},
}
),
{
name: 'app-storage',
storage: createJSONStorage(() => localStorage),
version: 2,
// Only persist specific fields
partialize: (state) => ({
theme: state.theme,
notifications: state.notifications,
// Don't persist: sidebarOpen (session-only UI state)
}),
// Handle migrations between versions
migrate: (persistedState: unknown, version: number) => {
const state = persistedState as Partial<AppState>;
if (version === 0) {
// v0 → v1: Added notifications
return {
...state,
notifications: { email: true, push: true, sms: false },
};
}
if (version === 1) {
// v1 → v2: Changed theme from boolean to union
return {
...state,
theme: (state as any).darkMode ? 'dark' : 'light',
};
}
return state as AppState;
},
// Called when hydration completes
onRehydrateStorage: () => (state, error) => {
if (error) {
console.error('Failed to rehydrate store:', error);
} else {
console.log('Store rehydrated:', state?.theme);
}
},
}
)
);
```
## subscribeWithSelector Usage
Enables subscribing to specific state slices outside React:
```typescript
// Subscribe to theme changes only
const unsubscribe = useAppStore.subscribe(
(state) => state.theme,
(theme, prevTheme) => {
console.log('Theme changed:', prevTheme, '→', theme);
document.documentElement.dataset.theme = theme;
},
{ fireImmediately: true }
);
// Subscribe with equality function
useAppStore.subscribe(
(state) => state.notifications,
(notifications) => {
syncNotificationsToServer(notifications);
},
{ equalityFn: shallow } // Only trigger if shallow-different
);
// Cleanup
unsubscribe();
```
## Devtools Best Practices
### Action Naming Convention
```typescript
// ✅ GOOD: Namespace/action format
set(state => { ... }, undefined, 'cart/addItem');
set(state => { ... }, undefined, 'auth/login');
set(state => { ... }, undefined, 'ui/toggleSidebar');
// ❌ BAD: No action name (shows as "anonymous")
set(state => { ... });
```
### Conditional DevTools
```typescript
const useStore = create<State>()(
devtools(
(set) => ({ ... }),
{
name: 'MyStore',
enabled: process.env.NODE_ENV === 'development',
// Trace calls for debugging (performance cost!)
trace: process.env.NODE_ENV === 'development',
traceLimit: 25,
}
)
);
```
## Persist Strategies
### Session Storage (Tab-Scoped)
```typescript
persist(
(set) => ({ ... }),
{
name: 'session-store',
storage: createJSONStorage(() => sessionStorage),
}
)
```
### IndexedDB (Large Data)
```typescript
import { get, set, del } from 'idb-keyval';
const indexedDBStorage = {
getItem: async (name: string) => {
return (await get(name)) ?? null;
},
setItem: async (name: string, value: string) => {
await set(name, value);
},
removeItem: async (name: string) => {
await del(name);
},
};
persist(
(set) => ({ ... }),
{
name: 'large-store',
storage: createJSONStorage(() => indexedDBStorage),
}
)
```
### Async Storage (React Native)
```typescript
import AsyncStorage from '@react-native-async-storage/async-storage';
persist(
(set) => ({ ... }),
{
name: 'mobile-store',
storage: createJSONStorage(() => AsyncStorage),
}
)
```
## Middleware Without Full Stack
### Immer Only (Simple Apps)
```typescript
const useStore = create<State>()(
immer((set) => ({
items: [],
addItem: (item) => set((state) => { state.items.push(item); }),
}))
);
```
### Persist Only (Simple Persistence)
```typescript
const useStore = create<State>()(
persist(
(set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}),
{ name: 'theme-storage' }
)
);
```
### DevTools Only (Development)
```typescript
const useStore = create<State>()(
devtools(
(set) => ({ ... }),
{ enabled: process.env.NODE_ENV === 'development' }
)
);
```
## TypeScript Typing for Middleware
When using multiple middleware, TypeScript needs explicit middleware type annotation:
```typescript
import { create, StateCreator } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
interface BearState {
bears: number;
increase: () => void;
}
// Explicit middleware types for slices
type BearSlice = StateCreator<
BearState,
[['zustand/immer', never], ['zustand/devtools', never]],
[],
BearState
>;
const createBearSlice: BearSlice = (set) => ({
bears: 0,
increase: () => set((state) => { state.bears += 1; }),
});
const useStore = create<BearState>()(
devtools(
immer(createBearSlice),
{ name: 'BearStore' }
)
);
```
## Common Pitfalls
### ❌ Wrong Order
```typescript
// WRONG: persist inside devtools
devtools(persist(immer(...))) // DevTools won't see persist actions
// CORRECT: persist outside devtools
persist(devtools(immer(...)))
```
### ❌ Duplicate Middleware
```typescript
// WRONG: Double-wrapping
persist(persist(...)) // Causes hydration issues
```
### ❌ Missing Type Import
```typescript
// WRONG: DevTools types missing
import { devtools } from 'zustand/middleware';
// CORRECT: Import type augmentation
import { devtools } from 'zustand/middleware';
import type {} from '@redux-devtools/extension';
```
rules/_sections.md
---
title: Zustand Patterns Rule Categories
version: 2.0.0
---
# Rule Categories
## 1. Middleware (zustand) — CRITICAL — 2 rules
Correct middleware composition order and common pitfalls.
- `zustand-middleware-order.md` — persist outermost, immer innermost, devtools/subscribeWithSelector in between
- `zustand-middleware-pitfalls.md` — Avoid direct mutation without immer, async middleware patterns, persist migrations
## 2. Store Architecture (zustand) — HIGH — 1 rule
Store organization patterns for maintainability.
- `zustand-slice-pattern.md` — Split large stores into typed slices, combine without circular dependencies
rules/_template.md
---
title: "[Rule Name]"
impact: "[CRITICAL | HIGH | MEDIUM | LOW]"
impactDescription: "[What goes wrong without this rule]"
tags: []
---
## [Rule Name]
[Brief description — 1-2 sentences.]
**Incorrect:**
```
// Bad pattern
```
**Correct:**
```
// Good pattern
```
**Key rules:**
- [Rule 1]
- [Rule 2]
- [Rule 3]
Reference: [link]
rules/zustand-middleware-order.md
---
title: Nest Zustand middleware in the correct order to prevent devtools and persist failures
category: zustand
impact: CRITICAL
impactDescription: "Wrong middleware nesting order causes devtools to miss actions, persist to serialize untransformed state, and immer drafts to leak outside their scope."
tags: [zustand, middleware, immer, devtools, persist]
---
## Zustand: Middleware Order
Zustand middleware wraps from inside out. The innermost middleware executes first, and the outermost middleware executes last. Getting this order wrong silently breaks persistence, devtools recording, and immutable updates.
**Incorrect:**
```typescript
// WRONG: immer outermost — draft mutations leak to devtools and persist
const useStore = create<AppState>()(
immer(devtools(persist((set) => ({
count: 0,
increment: () => set((state) => { state.count += 1; }),
}), { name: 'app-storage' }), { name: 'AppStore' }))
);
// WRONG: devtools inside persist — devtools won't see persist rehydration
devtools(persist(immer((set) => ({ /* ... */ })), { name: 'storage' }), { name: 'Store' });
```
**Correct:**
```typescript
import { create } from 'zustand';
import { devtools, persist, subscribeWithSelector } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import type {} from '@redux-devtools/extension';
// Correct order: persist > devtools > subscribeWithSelector > immer
const useStore = create<AppState>()(
persist(
devtools(
subscribeWithSelector(
immer((set) => ({
count: 0,
increment: () =>
set(
(state) => { state.count += 1; },
undefined,
'counter/increment'
),
}))
),
{ name: 'AppStore', enabled: process.env.NODE_ENV === 'development' }
),
{
name: 'app-storage',
partialize: (state) => ({ count: state.count }),
}
)
);
```
**Key rules:**
- **Immer** is always innermost -- transforms draft mutations into immutable updates first
- **subscribeWithSelector** wraps immer -- needs transformed (immutable) state for granular subscriptions
- **devtools** wraps subscribeWithSelector -- records actions after immer transforms them
- **persist** is always outermost -- serializes the final, fully transformed state to storage
- When using a subset, preserve relative order (e.g., `devtools(immer(...))` not `immer(devtools(...))`)
Reference: `references/middleware-composition.md` (Middleware Execution Order, Why Order Matters)
rules/zustand-middleware-pitfalls.md
---
title: Avoid Zustand middleware pitfalls that cause silent reactivity breaks and hydration failures
category: zustand
impact: HIGH
impactDescription: "Silent reactivity breaks, hydration failures, TS errors, and corrupted persisted state."
tags: [zustand, middleware, immer, persist, devtools, pitfalls]
---
## Zustand: Middleware Pitfalls
Four common middleware mistakes that cause silent bugs in Zustand stores.
### Pitfall 1: Mutating State Without Immer
**Incorrect:**
```typescript
set((state) => { state.items.push(item); return state; }); // Mutates in place, no re-render
```
**Correct:**
```typescript
set((state) => ({ items: [...state.items, item] })); // Immutable update
immer((set) => ({ addItem: (item) => set((s) => { s.items.push(item); }) })) // With immer
```
### Pitfall 2: Duplicate Middleware / Wrong Nesting
**Incorrect:**
```typescript
persist(persist((set) => ({ /* ... */ }), { name: 'a' }), { name: 'b' }) // Double-wrap
```
**Correct:**
```typescript
persist((set) => ({ /* ... */ }), { name: 'app-storage', partialize: (s) => ({ theme: s.theme }) })
```
### Pitfall 3: Missing DevTools Type Import
**Incorrect:**
```typescript
import { devtools } from 'zustand/middleware'; // TS errors — types not augmented
```
**Correct:**
```typescript
import { devtools } from 'zustand/middleware';
import type {} from '@redux-devtools/extension'; // Required type augmentation
```
### Pitfall 4: Missing Persist Version Migrations
**Incorrect:**
```typescript
persist((set) => ({ theme: 'light', fontSize: 14 }), { name: 'settings', version: 2 })
// Was version 1 — no migrate function, old state silently dropped
```
**Correct:**
```typescript
persist((set) => ({ theme: 'light', fontSize: 14 }), {
name: 'settings',
version: 2,
migrate: (persisted: unknown, version: number) => {
const state = persisted as Record<string, unknown>;
if (version === 1) return { ...state, fontSize: 14 }; // v1->v2: added fontSize
return state;
},
})
```
**Key rules:**
- Never use mutable methods (`push`, `splice`, property assignment) in `set()` without `immer` middleware
- Never double-wrap the same middleware -- each should appear exactly once
- Always `import type {} from '@redux-devtools/extension'` when using `devtools`
- Always provide a `migrate` function when bumping persist `version`
Reference: `references/middleware-composition.md` (Common Pitfalls)
rules/zustand-slice-pattern.md
---
title: Use the Zustand slice pattern to keep stores maintainable and avoid merge conflicts
category: zustand
impact: HIGH
impactDescription: "Monolithic stores become unmaintainable and cause merge conflicts. Missing slice types break TypeScript inference."
tags: [zustand, slices, architecture, StateCreator, modular]
---
## Zustand: Slice Pattern
Split large stores into typed slices using `StateCreator`. Each slice owns a domain of state and actions, combined into a single store at creation time.
**Incorrect:**
```typescript
// Monolithic store — all domains in one create() call
const useStore = create<AllState>()((set, get) => ({
user: null, token: null,
login: async (creds) => { /* ... */ },
logout: () => set({ user: null, token: null }),
items: [], addItem: (item) => set((s) => ({ items: [...s.items, item] })),
sidebarOpen: false, theme: 'light',
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
// ... 20 more fields — unmaintainable
}));
```
**Correct:**
```typescript
import { create, StateCreator } from 'zustand';
import { immer } from 'zustand/middleware/immer';
type AppStore = AuthSlice & CartSlice & UISlice;
// --- Auth Slice (store/auth-slice.ts) ---
interface AuthSlice { user: User | null; login: (creds: Credentials) => Promise<void>; logout: () => void; }
const createAuthSlice: StateCreator<
AppStore, [['zustand/immer', never]], [], AuthSlice
> = (set) => ({
user: null,
login: async (creds) => { set({ user: await api.login(creds) }, undefined, 'auth/login'); },
logout: () => set((s) => { s.user = null; }, undefined, 'auth/logout'),
});
// --- Cart Slice (cross-slice access via get()) ---
interface CartSlice { items: CartItem[]; addItem: (item: CartItem) => void; }
const createCartSlice: StateCreator<
AppStore, [['zustand/immer', never]], [], CartSlice
> = (set, get) => ({
items: [],
addItem: (item) => {
if (!get().user) return; // Cross-slice access via get()
set((s) => { s.items.push(item); }, undefined, 'cart/addItem');
},
});
// --- Combined Store ---
const useStore = create<AppStore>()(
immer((...a) => ({
...createAuthSlice(...a),
...createCartSlice(...a),
...createUISlice(...a),
}))
);
```
**Key rules:**
- Type each slice as `StateCreator<CombinedStore, MiddlewareMutators, [], SliceInterface>` for full store type inference
- Combine with spread: `create<Store>()((...a) => ({ ...createSliceA(...a), ...createSliceB(...a) }))` -- `...a` forwards `set`, `get`, `store`
- Access other slices via `get()` inside actions, never by importing state directly -- avoids circular dependencies
- Keep each slice in its own file, export only the creator function and interface
- Declare middleware mutator types in the `StateCreator` generic so TypeScript knows available features
Reference: `references/middleware-composition.md` (TypeScript Typing for Middleware)
scripts/store-template.ts
/**
* Production-ready Zustand store template
*
* Features:
* - TypeScript strict mode
* - Slices pattern for modularity
* - Immer for immutable updates
* - DevTools for debugging
* - Persist with migrations
* - Proper selector exports
* - Reset functionality
*
* Usage:
* 1. Copy this template
* 2. Modify slices for your domain
* 3. Update persistence partialize
* 4. Add your selectors
*/
import { create, StateCreator } from 'zustand';
import { devtools, persist, subscribeWithSelector, createJSONStorage } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import { useShallow } from 'zustand/react/shallow';
import type {} from '@redux-devtools/extension';
// ============================================
// Types
// ============================================
interface User {
id: string;
email: string;
name: string;
avatar?: string;
}
interface CartItem {
id: string;
productId: string;
name: string;
price: number;
quantity: number;
}
interface Notification {
id: string;
type: 'info' | 'success' | 'warning' | 'error';
message: string;
timestamp: number;
}
// ============================================
// Slice Interfaces
// ============================================
interface AuthSlice {
// State
user: User | null;
isAuthenticated: boolean;
// Actions
login: (user: User) => void;
logout: () => void;
updateProfile: (updates: Partial<User>) => void;
}
interface CartSlice {
// State
items: CartItem[];
// Actions
addItem: (item: Omit<CartItem, 'id'>) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
clearCart: () => void;
}
interface UISlice {
// State
sidebarOpen: boolean;
theme: 'light' | 'dark' | 'system';
notifications: Notification[];
// Actions
toggleSidebar: () => void;
setSidebarOpen: (open: boolean) => void;
setTheme: (theme: 'light' | 'dark' | 'system') => void;
addNotification: (notification: Omit<Notification, 'id' | 'timestamp'>) => void;
dismissNotification: (id: string) => void;
clearNotifications: () => void;
}
interface ResetSlice {
reset: () => void;
}
// Combined store type
type StoreState = AuthSlice & CartSlice & UISlice & ResetSlice;
// Middleware type for slices
type SliceCreator<T> = StateCreator<
StoreState,
[['zustand/immer', never], ['zustand/devtools', never]],
[],
T
>;
// ============================================
// Initial State (for reset)
// ============================================
const initialAuthState: Pick<AuthSlice, 'user' | 'isAuthenticated'> = {
user: null,
isAuthenticated: false,
};
const initialCartState: Pick<CartSlice, 'items'> = {
items: [],
};
const initialUIState: Pick<UISlice, 'sidebarOpen' | 'theme' | 'notifications'> = {
sidebarOpen: true,
theme: 'system',
notifications: [],
};
// ============================================
// Slice Creators
// ============================================
const createAuthSlice: SliceCreator<AuthSlice> = (set) => ({
...initialAuthState,
login: (user) =>
set(
(state) => {
state.user = user;
state.isAuthenticated = true;
},
undefined,
'auth/login'
),
logout: () =>
set(
(state) => {
state.user = null;
state.isAuthenticated = false;
state.items = []; // Clear cart on logout
},
undefined,
'auth/logout'
),
updateProfile: (updates) =>
set(
(state) => {
if (state.user) {
Object.assign(state.user, updates);
}
},
undefined,
'auth/updateProfile'
),
});
const createCartSlice: SliceCreator<CartSlice> = (set) => ({
...initialCartState,
addItem: (item) =>
set(
(state) => {
const existing = state.items.find((i) => i.productId === item.productId);
if (existing) {
existing.quantity += item.quantity;
} else {
state.items.push({
...item,
id: crypto.randomUUID(),
});
}
},
undefined,
'cart/addItem'
),
removeItem: (id) =>
set(
(state) => {
state.items = state.items.filter((i) => i.id !== id);
},
undefined,
'cart/removeItem'
),
updateQuantity: (id, quantity) =>
set(
(state) => {
const item = state.items.find((i) => i.id === id);
if (item) {
if (quantity <= 0) {
state.items = state.items.filter((i) => i.id !== id);
} else {
item.quantity = quantity;
}
}
},
undefined,
'cart/updateQuantity'
),
clearCart: () =>
set(
(state) => {
state.items = [];
},
undefined,
'cart/clear'
),
});
const createUISlice: SliceCreator<UISlice> = (set) => ({
...initialUIState,
toggleSidebar: () =>
set(
(state) => {
state.sidebarOpen = !state.sidebarOpen;
},
undefined,
'ui/toggleSidebar'
),
setSidebarOpen: (open) =>
set(
(state) => {
state.sidebarOpen = open;
},
undefined,
'ui/setSidebarOpen'
),
setTheme: (theme) =>
set(
(state) => {
state.theme = theme;
},
undefined,
'ui/setTheme'
),
addNotification: (notification) =>
set(
(state) => {
state.notifications.push({
...notification,
id: crypto.randomUUID(),
timestamp: Date.now(),
});
},
undefined,
'ui/addNotification'
),
dismissNotification: (id) =>
set(
(state) => {
state.notifications = state.notifications.filter((n) => n.id !== id);
},
undefined,
'ui/dismissNotification'
),
clearNotifications: () =>
set(
(state) => {
state.notifications = [];
},
undefined,
'ui/clearNotifications'
),
});
// ============================================
// Store Creation
// ============================================
export const useAppStore = create<StoreState>()(
persist(
devtools(
subscribeWithSelector(
immer((...args) => ({
...createAuthSlice(...args),
...createCartSlice(...args),
...createUISlice(...args),
// Reset all state
reset: () =>
args[0](
() => ({
...initialAuthState,
...initialCartState,
...initialUIState,
}),
true,
'app/reset'
),
}))
),
{
name: 'AppStore',
enabled: process.env.NODE_ENV === 'development',
}
),
{
name: 'app-storage',
storage: createJSONStorage(() => localStorage),
version: 1,
// Only persist these fields
partialize: (state) => ({
user: state.user,
isAuthenticated: state.isAuthenticated,
items: state.items,
theme: state.theme,
// Don't persist: sidebarOpen, notifications (ephemeral)
}),
// Handle schema migrations
migrate: (persisted: unknown, version: number) => {
const state = persisted as Partial<StoreState>;
// Add migration logic here as schema evolves
// if (version === 0) { ... }
return state as StoreState;
},
}
)
);
// ============================================
// Selectors (Prevent Re-renders)
// ============================================
// Auth selectors
export const useUser = () => useAppStore((s) => s.user);
export const useIsAuthenticated = () => useAppStore((s) => s.isAuthenticated);
// Cart selectors
export const useCartItems = () => useAppStore((s) => s.items);
export const useCartItemCount = () => useAppStore((s) => s.items.length);
export const useCartTotal = () =>
useAppStore((s) => s.items.reduce((sum, item) => sum + item.price * item.quantity, 0));
// UI selectors
export const useSidebarOpen = () => useAppStore((s) => s.sidebarOpen);
export const useTheme = () => useAppStore((s) => s.theme);
export const useNotifications = () => useAppStore((s) => s.notifications);
// Multi-value selectors (use useShallow)
export const useAuth = () =>
useAppStore(useShallow((s) => ({ user: s.user, isAuthenticated: s.isAuthenticated })));
export const useCartSummary = () =>
useAppStore(
useShallow((s) => ({
itemCount: s.items.length,
total: s.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
}))
);
// ============================================
// Action Selectors (Stable References)
// ============================================
export const useAuthActions = () =>
useAppStore(useShallow((s) => ({ login: s.login, logout: s.logout, updateProfile: s.updateProfile })));
export const useCartActions = () =>
useAppStore(
useShallow((s) => ({
addItem: s.addItem,
removeItem: s.removeItem,
updateQuantity: s.updateQuantity,
clearCart: s.clearCart,
}))
);
export const useUIActions = () =>
useAppStore(
useShallow((s) => ({
toggleSidebar: s.toggleSidebar,
setSidebarOpen: s.setSidebarOpen,
setTheme: s.setTheme,
addNotification: s.addNotification,
dismissNotification: s.dismissNotification,
clearNotifications: s.clearNotifications,
}))
);
// ============================================
// External Subscriptions (Non-React)
// ============================================
// Subscribe to theme changes for document updates
if (typeof window !== 'undefined') {
useAppStore.subscribe(
(state) => state.theme,
(theme) => {
const resolvedTheme =
theme === 'system'
? window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light'
: theme;
document.documentElement.dataset.theme = resolvedTheme;
},
{ fireImmediately: true }
);
}
// ============================================
// Testing Utilities
// ============================================
export const resetStore = () => useAppStore.getState().reset();
export const getStoreSnapshot = () => useAppStore.getState();
// For testing: replace store state
export const setStoreState = (state: Partial<StoreState>) => {
useAppStore.setState(state);
};
test-cases.json
{
"skill": "zustand-patterns",
"version": "1.0.0",
"testCases": [
{
"id": "negative-i-have-a-react",
"rule": "",
"query": "I have a React app with auth state and a shopping cart that need to share a store. The cart has deeply nested item variants. Set up the Zustand store with proper TypeScript types.",
"expectedBehavior": [
"Claude uses the slices pattern with separate AuthSlice and CartSlice using StateCreator",
"Combines slices into a single store with create<AuthSlice & CartSlice>()",
"Applies Immer middleware for deeply nested cart item variant updates",
"Uses proper TypeScript generics with the double-call pattern create<State>()()",
"Does NOT destructure the entire store; uses individual selectors or useShallow"
]
},
{
"id": "negative-my-zustand-store-rerenders",
"rule": "",
"query": "My Zustand store re-renders every component whenever any state changes. I'm selecting 3 values from the store in each component. How do I fix the performance issue?",
"expectedBehavior": [
"Claude identifies the anti-pattern of destructuring the entire store",
"Recommends useShallow from zustand/react/shallow for selecting multiple values",
"Does NOT use the deprecated shallow import from zustand/shallow",
"Shows the correct pattern: useShallow((state) => ({ a: state.a, b: state.b, c: state.c }))",
"Explains that computed/derived state should use selectors, not stored values"
]
},
{
"id": "negative-set-up-a-postgresql",
"rule": "",
"query": "Set up a PostgreSQL connection pool with retry logic for our Node.js backend",
"expectedBehavior": [
"Claude does NOT invoke the zustand-patterns skill",
"Implements database connection pooling using standard Node.js patterns",
"Uses backend infrastructure tools without any Zustand or React state management"
]
},
{
"id": "zustand-middleware-order",
"rule": "zustand-middleware-order",
"query": "I'm setting up a Zustand store with persist, devtools, and immer middleware. What order should I nest them in?",
"expectedBehavior": [
"Specifies the correct nesting order: persist outermost, then devtools, then subscribeWithSelector, then immer innermost",
"Explains that immer must be innermost to transform draft mutations before other middleware sees them",
"Shows that persist must be outermost to serialize fully transformed state to storage",
"Warns that wrong order causes devtools to miss actions and persist to serialize untransformed state"
]
},
{
"id": "zustand-middleware-pitfalls",
"rule": "zustand-middleware-pitfalls",
"query": "My Zustand store state changes aren't persisting between page reloads and I'm getting TypeScript errors with devtools. What am I doing wrong?",
"expectedBehavior": [
"Checks for missing persist version migration function when bumping the version number",
"Verifies the devtools type augmentation import is present for TypeScript compatibility",
"Warns against mutating state with push or splice without immer middleware enabled",
"Checks for double-wrapping the same middleware which causes silent state corruption"
]
},
{
"id": "zustand-slice-pattern",
"rule": "zustand-slice-pattern",
"query": "Our Zustand store has grown to 30+ fields across auth, cart, and UI concerns. How should we restructure it?",
"expectedBehavior": [
"Recommends splitting into typed slices using StateCreator with proper generic parameters",
"Shows how to combine slices with spread syntax forwarding set, get, and store arguments",
"Advises using get() for cross-slice access inside actions to avoid circular dependencies",
"Keeps each slice in its own file exporting only the creator function and interface"
]
}
]
}