Zustand manages React state with minimal boilerplate — a create() call defines a store with state and actions in one object. The slice pattern composes multiple state chunks into a single store. immer middleware enables mutation-style state updates for complex nested objects. persist serializes state to localStorage, sessionStorage, or custom storage adapters for cross-session persistence. subscribeWithSelector subscribes to specific state slices without re-rendering on unrelated changes. devtools exposes the store to Redux DevTools for time-travel debugging. TypeScript generics infer the full store type from the initial state. Zustand stores can be used outside React — plain getState() and subscribe() calls work in any context. Claude Code generates Zustand store definitions, slice patterns, middleware compositions, TypeScript-safe selectors, and the persistence configurations for production React applications.
CLAUDE.md for Zustand
## Zustand Stack
- Version: zustand >= 5.0
- Create: create<StoreType>()(immer(devtools(persist(...)))) — compose middleware
- Slices: combine slices with combineSlices() or spread pattern for modular stores
- Selectors: useStore(s => s.value) — select deeply, shallow() for objects
- Persist: persist(stateCreator, { name: "store", storage }) — localStorage/IndexedDB
- Immer: immer middleware — mutate draft directly, Immer creates immutable update
- Devtools: devtools(creator, { name: "MyStore" }) — Redux DevTools integration
- Testing: const store = createStore(stateCreator) — test outside React
Slice Pattern
// stores/slices/orders-slice.ts — orders slice
import type { StateCreator } from "zustand"
export interface Order {
id: string
status: "pending" | "processing" | "shipped" | "delivered" | "cancelled"
totalCents: number
customerId: string
createdAt: string
}
export interface OrdersSlice {
orders: Order[]
ordersLoading: boolean
ordersError: string | null
// Actions
fetchOrders: (customerId: string) => Promise<void>
cancelOrder: (orderId: string) => Promise<void>
updateOrderStatus: (orderId: string, status: Order["status"]) => void
}
export const createOrdersSlice: StateCreator<
OrdersSlice & CartSlice, // Access full store type for cross-slice actions
[["zustand/immer", never], ["zustand/devtools", never]],
[],
OrdersSlice
> = (set, get) => ({
orders: [],
ordersLoading: false,
ordersError: null,
fetchOrders: async (customerId) => {
set(state => { state.ordersLoading = true; state.ordersError = null })
try {
const response = await fetch(`/api/orders?customerId=${customerId}`)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const orders = await response.json()
set(state => {
state.orders = orders
state.ordersLoading = false
})
} catch (error) {
set(state => {
state.ordersError = (error as Error).message
state.ordersLoading = false
})
}
},
cancelOrder: async (orderId) => {
await fetch(`/api/orders/${orderId}/cancel`, { method: "POST" })
// Immer: direct mutation — no spread needed
set(state => {
const order = state.orders.find(o => o.id === orderId)
if (order) order.status = "cancelled"
})
// Cross-slice: clear cart items for this order
get().clearOrderItems(orderId)
},
updateOrderStatus: (orderId, status) => {
set(state => {
const order = state.orders.find(o => o.id === orderId)
if (order) order.status = status
})
},
})
// stores/slices/cart-slice.ts — cart slice
import type { StateCreator } from "zustand"
export interface CartItem {
productId: string
name: string
priceCents: number
quantity: number
}
export interface CartSlice {
cartItems: CartItem[]
couponCode: string | null
discountPercent: number
// Computed (in selector, not slice)
addToCart: (item: Omit<CartItem, "quantity">) => void
removeFromCart: (productId: string) => void
updateQuantity: (productId: string, quantity: number) => void
applyCoupon: (code: string) => Promise<void>
clearCart: () => void
clearOrderItems: (orderId: string) => void
}
export const createCartSlice: StateCreator<
CartSlice & OrdersSlice,
[["zustand/immer", never], ["zustand/devtools", never], ["zustand/persist", unknown]],
[],
CartSlice
> = (set) => ({
cartItems: [],
couponCode: null,
discountPercent: 0,
addToCart: (item) => {
set(state => {
const existing = state.cartItems.find(i => i.productId === item.productId)
if (existing) {
existing.quantity += 1
} else {
state.cartItems.push({ ...item, quantity: 1 })
}
})
},
removeFromCart: (productId) => {
set(state => {
state.cartItems = state.cartItems.filter(i => i.productId !== productId)
})
},
updateQuantity: (productId, quantity) => {
set(state => {
if (quantity <= 0) {
state.cartItems = state.cartItems.filter(i => i.productId !== productId)
} else {
const item = state.cartItems.find(i => i.productId === productId)
if (item) item.quantity = quantity
}
})
},
applyCoupon: async (code) => {
const response = await fetch(`/api/coupons/${code}`)
if (!response.ok) throw new Error("Invalid coupon")
const { discount } = await response.json()
set(state => {
state.couponCode = code
state.discountPercent = discount
})
},
clearCart: () => {
set(state => {
state.cartItems = []
state.couponCode = null
state.discountPercent = 0
})
},
clearOrderItems: (_orderId) => {
// Called from orders slice after cancel
set(state => { state.cartItems = [] })
},
})
Combined Store with Middleware
// stores/index.ts — combine slices with middleware
import { create } from "zustand"
import { immer } from "zustand/middleware/immer"
import { devtools, persist } from "zustand/middleware"
import { subscribeWithSelector } from "zustand/middleware"
import type { OrdersSlice } from "./slices/orders-slice"
import type { CartSlice } from "./slices/cart-slice"
import { createOrdersSlice } from "./slices/orders-slice"
import { createCartSlice } from "./slices/cart-slice"
export type AppStore = OrdersSlice & CartSlice
export const useAppStore = create<AppStore>()(
devtools(
persist(
subscribeWithSelector(
immer((...args) => ({
...createOrdersSlice(...args),
...createCartSlice(...args),
}))
),
{
name: "app-store",
// Only persist cart — not fetched orders
partialize: (state) => ({
cartItems: state.cartItems,
couponCode: state.couponCode,
discountPercent: state.discountPercent,
}),
version: 1,
// Migrate old persisted state if schema changes
migrate: (persistedState: any, version) => {
if (version === 0) {
return { ...persistedState, discountPercent: 0 }
}
return persistedState
},
}
),
{ name: "AppStore" }
)
)
// Computed selectors — outside store for memoization
export const selectCartTotal = (state: AppStore) =>
state.cartItems.reduce(
(sum, item) => sum + item.priceCents * item.quantity,
0
)
export const selectCartDiscount = (state: AppStore) =>
Math.round(selectCartTotal(state) * state.discountPercent / 100)
export const selectCartFinalTotal = (state: AppStore) =>
selectCartTotal(state) - selectCartDiscount(state)
// Subscribe to cart changes outside React
useAppStore.subscribe(
state => state.cartItems.length,
(count) => {
document.title = count > 0 ? `Cart (${count}) — MyStore` : "MyStore"
}
)
React Components
// components/CartSummary.tsx — shallow selector for objects
import { useAppStore, selectCartTotal, selectCartFinalTotal } from "@/stores"
import { shallow } from "zustand/shallow"
export function CartSummary() {
// shallow: re-renders only when these specific values change
const { cartItems, couponCode, discountPercent } = useAppStore(
state => ({
cartItems: state.cartItems,
couponCode: state.couponCode,
discountPercent: state.discountPercent,
}),
shallow
)
// Individual selector — primitives use reference equality
const total = useAppStore(selectCartTotal)
const finalTotal = useAppStore(selectCartFinalTotal)
const clearCart = useAppStore(state => state.clearCart)
return (
<div>
<p>{cartItems.length} items</p>
{couponCode && <p>Coupon: {couponCode} (-{discountPercent}%)</p>}
<p>Subtotal: ${(total / 100).toFixed(2)}</p>
<p>Total: ${(finalTotal / 100).toFixed(2)}</p>
<button onClick={clearCart}>Clear cart</button>
</div>
)
}
Store Testing
// tests/orders-store.test.ts — test store without React
import { createStore } from "zustand/vanilla"
import { immer } from "zustand/middleware/immer"
import { createOrdersSlice } from "../stores/slices/orders-slice"
describe("OrdersSlice", () => {
it("updates order status optimistically", () => {
const store = createStore(immer(createOrdersSlice))
store.setState(state => {
state.orders = [
{ id: "ord-1", status: "pending", totalCents: 1000, customerId: "cust-1", createdAt: "" }
]
})
store.getState().updateOrderStatus("ord-1", "shipped")
const { orders } = store.getState()
expect(orders[0].status).toBe("shipped")
})
})
For the Jotai atomic state alternative that decomposes state into atoms — better for highly granular subscriptions where components read independent state slices, see the React state management guide for atom patterns. For the Redux Toolkit alternative when a structured actions/reducers model with better DevTools time-travel and Redux ecosystem (redux-saga, RTK Query) is required, the Redux guide covers RTK patterns. The Claude Skills 360 bundle includes Zustand skill sets covering slices, middleware, and persist configuration. Start with the free tier to try Zustand store generation.