toolchains/typescript/state/zustand/SKILL.md
Minimal, unopinionated state management library for React with simple hook-based API, no providers, and minimal boilerplate for global state without Redux complexity.
npx skillsauth add bobmatnyc/claude-mpm-skills zustandInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Zustand is a minimal, unopinionated state management library for React. No providers, no boilerplate—just a simple hook-based API that feels natural in React applications.
npm install zustand
// stores/useCounterStore.ts
import { create } from 'zustand'
interface CounterState {
count: number
increment: () => void
decrement: () => void
}
export const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))
// components/Counter.tsx
import { useCounterStore } from '@/stores/useCounterStore'
export function Counter() {
const { count, increment, decrement } = useCounterStore()
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
)
}
import { create } from 'zustand'
// Basic store
interface BearState {
bears: number
addBear: () => void
}
const useBearStore = create<BearState>((set) => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 })),
}))
// Store with get access
const useStore = create<State>((set, get) => ({
count: 0,
increment: () => {
const currentCount = get().count
set({ count: currentCount + 1 })
},
}))
// Select entire store (re-renders on any change)
const state = useStore()
// Select specific fields (re-renders only when these change)
const bears = useStore((state) => state.bears)
const addBear = useStore((state) => state.addBear)
// Destructure with selector
const { bears, addBear } = useStore((state) => ({
bears: state.bears,
addBear: state.addBear,
}))
// Multiple selectors
const bears = useStore((state) => state.bears)
const fish = useStore((state) => state.fish)
interface TodoState {
todos: Todo[]
addTodo: (text: string) => void
toggleTodo: (id: string) => void
removeTodo: (id: string) => void
}
const useTodoStore = create<TodoState>((set) => ({
todos: [],
// Add item
addTodo: (text) => set((state) => ({
todos: [...state.todos, { id: nanoid(), text, completed: false }]
})),
// Update item
toggleTodo: (id) => set((state) => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
})),
// Remove item
removeTodo: (id) => set((state) => ({
todos: state.todos.filter(todo => todo.id !== id)
})),
}))
function BearCounter() {
// Re-renders when bears changes
const bears = useBearStore((state) => state.bears)
return <h1>{bears} bears around here...</h1>
}
function Controls() {
// Doesn't re-render when bears changes
const addBear = useBearStore((state) => state.addBear)
return <button onClick={addBear}>Add bear</button>
}
import { shallow } from 'zustand/shallow'
// Prevent re-renders when object identity changes but values don't
const { nuts, honey } = useBearStore(
(state) => ({ nuts: state.nuts, honey: state.honey }),
shallow
)
// Custom equality function
const treats = useBearStore(
(state) => state.treats,
(prev, next) => prev.length === next.length
)
// Read state
const count = useStore.getState().count
// Subscribe to changes
const unsubscribe = useStore.subscribe(
(state) => console.log('Count changed:', state.count)
)
// Update state
useStore.setState({ count: 42 })
// Update with function
useStore.setState((state) => ({ count: state.count + 1 }))
interface UserState {
user: User | null
setUser: (user: User) => void
clearUser: () => void
}
const useUserStore = create<UserState>((set) => ({
user: null,
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
}))
// Type inference works automatically
const user = useUserStore((state) => state.user) // User | null
// Extract store type
type UserStoreState = ReturnType<typeof useUserStore.getState>
// Selector type helper
type Selector<T> = (state: UserState) => T
const selectUsername: Selector<string | undefined> = (state) =>
state.user?.name
// Type-safe store combination
function useHybridStore<T, U>(
selector1: (state: State1) => T,
selector2: (state: State2) => U
): [T, U] {
return [
useStore1(selector1),
useStore2(selector2),
]
}
const [user, theme] = useHybridStore(
(s) => s.user,
(s) => s.theme
)
// authSlice.ts
export interface AuthSlice {
user: User | null
login: (credentials: Credentials) => Promise<void>
logout: () => void
}
export const createAuthSlice: StateCreator<
AuthSlice & TodoSlice,
[],
[],
AuthSlice
> = (set) => ({
user: null,
login: async (credentials) => {
const user = await api.login(credentials)
set({ user })
},
logout: () => set({ user: null }),
})
// todoSlice.ts
export interface TodoSlice {
todos: Todo[]
addTodo: (text: string) => void
}
export const createTodoSlice: StateCreator<
AuthSlice & TodoSlice,
[],
[],
TodoSlice
> = (set) => ({
todos: [],
addTodo: (text) => set((state) => ({
todos: [...state.todos, { id: nanoid(), text, completed: false }]
})),
})
// store.ts
import { create } from 'zustand'
import { createAuthSlice, AuthSlice } from './authSlice'
import { createTodoSlice, TodoSlice } from './todoSlice'
export const useStore = create<AuthSlice & TodoSlice>()((...a) => ({
...createAuthSlice(...a),
...createTodoSlice(...a),
}))
export const createTodoSlice: StateCreator<
AuthSlice & TodoSlice,
[],
[],
TodoSlice
> = (set, get) => ({
todos: [],
addTodo: (text) => {
// Access other slice's state
const user = get().user
if (!user) throw new Error('Not authenticated')
set((state) => ({
todos: [...state.todos, {
id: nanoid(),
text,
userId: user.id,
completed: false
}]
}))
},
})
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
interface PreferencesState {
theme: 'light' | 'dark'
language: string
setTheme: (theme: 'light' | 'dark') => void
}
export const usePreferencesStore = create<PreferencesState>()(
persist(
(set) => ({
theme: 'light',
language: 'en',
setTheme: (theme) => set({ theme }),
}),
{
name: 'preferences-storage', // localStorage key
storage: createJSONStorage(() => localStorage),
// Partial persistence
partialize: (state) => ({ theme: state.theme }),
// Migration between versions
version: 1,
migrate: (persistedState: any, version: number) => {
if (version === 0) {
// Migrate from v0 to v1
persistedState.language = 'en'
}
return persistedState as PreferencesState
},
}
)
)
// Custom storage (e.g., AsyncStorage for React Native)
const customStorage = {
getItem: async (name: string) => {
const value = await AsyncStorage.getItem(name)
return value ?? null
},
setItem: async (name: string, value: string) => {
await AsyncStorage.setItem(name, value)
},
removeItem: async (name: string) => {
await AsyncStorage.removeItem(name)
},
}
const useStore = create(
persist(
(set) => ({ /* ... */ }),
{
name: 'app-storage',
storage: createJSONStorage(() => customStorage)
}
)
)
import { devtools } from 'zustand/middleware'
interface CounterState {
count: number
increment: () => void
}
const useCounterStore = create<CounterState>()(
devtools(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }), false, 'increment'),
}),
{
name: 'CounterStore',
enabled: process.env.NODE_ENV === 'development'
}
)
)
// Action names in Redux DevTools
set({ count: 42 }, false, 'setCount')
set((state) => ({ count: state.count + 1 }), false, { type: 'increment', amount: 1 })
import { immer } from 'zustand/middleware/immer'
interface TodoState {
todos: Todo[]
addTodo: (text: string) => void
toggleTodo: (id: string) => void
}
const useTodoStore = create<TodoState>()(
immer((set) => ({
todos: [],
// Mutate state directly with Immer
addTodo: (text) => set((state) => {
state.todos.push({ id: nanoid(), text, completed: false })
}),
toggleTodo: (id) => set((state) => {
const todo = state.todos.find(t => t.id === id)
if (todo) todo.completed = !todo.completed
}),
}))
)
const useStore = create<State>()(
devtools(
persist(
immer((set) => ({
// Store implementation
})),
{ name: 'app-storage' }
),
{ name: 'AppStore' }
)
)
interface UserState {
users: User[]
loading: boolean
error: string | null
fetchUsers: () => Promise<void>
}
const useUserStore = create<UserState>((set) => ({
users: [],
loading: false,
error: null,
fetchUsers: async () => {
set({ loading: true, error: null })
try {
const users = await api.getUsers()
set({ users, loading: false })
} catch (error) {
set({ error: error.message, loading: false })
}
},
}))
interface TodoState {
todos: Todo[]
addTodo: (text: string) => Promise<void>
}
const useTodoStore = create<TodoState>((set, get) => ({
todos: [],
addTodo: async (text) => {
const tempId = `temp-${Date.now()}`
const optimisticTodo = { id: tempId, text, completed: false }
// Add optimistically
set((state) => ({ todos: [...state.todos, optimisticTodo] }))
try {
const savedTodo = await api.createTodo(text)
// Replace temp with real todo
set((state) => ({
todos: state.todos.map(t =>
t.id === tempId ? savedTodo : t
)
}))
} catch (error) {
// Rollback on error
set((state) => ({
todos: state.todos.filter(t => t.id !== tempId)
}))
throw error
}
},
}))
interface DataState {
data: Data | null
loading: boolean
fetchData: () => Promise<void>
}
let currentRequest: Promise<void> | null = null
const useDataStore = create<DataState>((set) => ({
data: null,
loading: false,
fetchData: async () => {
// Return existing request if in progress
if (currentRequest) return currentRequest
set({ loading: true })
currentRequest = api.getData()
.then((data) => {
set({ data, loading: false })
})
.catch((error) => {
set({ loading: false })
throw error
})
.finally(() => {
currentRequest = null
})
return currentRequest
},
}))
interface TodoState {
todos: Todo[]
}
// Memoized with useCallback or outside component
const selectCompletedCount = (state: TodoState) =>
state.todos.filter(t => t.completed).length
const selectActiveCount = (state: TodoState) =>
state.todos.filter(t => !t.completed).length
function TodoStats() {
const completedCount = useTodoStore(selectCompletedCount)
const activeCount = useTodoStore(selectActiveCount)
return <div>{completedCount} / {activeCount + completedCount}</div>
}
interface TodoState {
todos: Todo[]
get completed(): Todo[]
get active(): Todo[]
get stats(): { total: number; completed: number; active: number }
}
const useTodoStore = create<TodoState>((set, get) => ({
todos: [],
get completed() {
return get().todos.filter(t => t.completed)
},
get active() {
return get().todos.filter(t => !t.completed)
},
get stats() {
const todos = get().todos
return {
total: todos.length,
completed: todos.filter(t => t.completed).length,
active: todos.filter(t => !t.completed).length,
}
},
}))
// Usage
const stats = useTodoStore((state) => state.stats)
// Create selector factory
const selectTodoById = (id: string) => (state: TodoState) =>
state.todos.find(t => t.id === id)
function TodoItem({ id }: { id: string }) {
const todo = useTodoStore(selectTodoById(id))
return <div>{todo?.text}</div>
}
// Subscribe to specific state changes
useEffect(() => {
const unsubscribe = useTodoStore.subscribe(
(state) => state.todos,
(todos) => {
console.log('Todos changed:', todos)
}
)
return unsubscribe
}, [])
// Subscribe with selector and equality
const unsubscribe = useTodoStore.subscribe(
(state) => state.todos.length,
(length) => console.log('Todo count:', length),
{ equalityFn: (a, b) => a === b }
)
// Updates that don't trigger subscribers
interface ScrubbingState {
position: number
updatePosition: (pos: number) => void
}
const useScrubbingStore = create<ScrubbingState>((set) => ({
position: 0,
updatePosition: (pos) => set({ position: pos }, true), // true = transient
}))
// Subscribers won't be notified
useScrubbingStore.getState().updatePosition(50)
const useTodoStore = create<TodoState>((set) => ({
todos: [],
batchUpdate: (updates: Partial<TodoState>[]) => {
// Single re-render for multiple updates
set((state) => {
let newState = { ...state }
updates.forEach(update => {
newState = { ...newState, ...update }
})
return newState
})
},
}))
// __tests__/Counter.test.tsx
import { create } from 'zustand'
import { render, screen, fireEvent } from '@testing-library/react'
import { Counter } from '@/components/Counter'
import { useCounterStore } from '@/stores/useCounterStore'
// Mock the store
jest.mock('@/stores/useCounterStore')
describe('Counter', () => {
beforeEach(() => {
const mockStore = create<CounterState>((set) => ({
count: 0,
increment: jest.fn(() => set((state) => ({ count: state.count + 1 }))),
decrement: jest.fn(),
}))
useCounterStore.mockImplementation(mockStore)
})
it('increments count', () => {
render(<Counter />)
fireEvent.click(screen.getByText('+'))
expect(screen.getByText('Count: 1')).toBeInTheDocument()
})
})
// test-utils.ts
import { create } from 'zustand'
export function createTestStore<T>(initialState: Partial<T>) {
return create<T>(() => initialState as T)
}
// Usage in tests
const testStore = createTestStore<TodoState>({
todos: [
{ id: '1', text: 'Test todo', completed: false }
]
})
// stores/useCounterStore.ts
const initialState = { count: 0 }
export const useCounterStore = create<CounterState>((set) => ({
...initialState,
increment: () => set((state) => ({ count: state.count + 1 })),
reset: () => set(initialState),
}))
// __tests__/Counter.test.tsx
afterEach(() => {
useCounterStore.getState().reset()
})
// Redux
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1 },
decrement: (state) => { state.value -= 1 },
},
})
// Zustand equivalent
const useCounterStore = create<CounterState>((set) => ({
value: 0,
increment: () => set((state) => ({ value: state.value + 1 })),
decrement: () => set((state) => ({ value: state.value - 1 })),
}))
// Redux usage
const dispatch = useDispatch()
const value = useSelector((state) => state.counter.value)
dispatch(increment())
// Zustand usage
const { value, increment } = useCounterStore()
increment()
// Context API
const ThemeContext = createContext<ThemeContextType>(null!)
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>('light')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
)
}
export const useTheme = () => useContext(ThemeContext)
// Zustand equivalent (no provider needed!)
export const useThemeStore = create<ThemeState>((set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}))
// Usage is simpler
const { theme, setTheme } = useThemeStore()
// stores/useCartStore.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
export const useCartStore = create<CartState>()(
persist(
(set) => ({
items: [],
addItem: (item) => set((state) => ({
items: [...state.items, item]
})),
}),
{
name: 'cart-storage',
// Skip persistence on server
skipHydration: true,
}
)
)
// components/Cart.tsx (Client Component)
'use client'
import { useCartStore } from '@/stores/useCartStore'
import { useEffect } from 'react'
export function Cart() {
const { items, addItem } = useCartStore()
// Hydrate persisted state
useEffect(() => {
useCartStore.persist.rehydrate()
}, [])
return <div>{items.length} items</div>
}
// actions/cart.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function syncCartToServer(items: CartItem[]) {
await db.cart.upsert({
where: { userId: 'current-user' },
update: { items },
create: { userId: 'current-user', items },
})
revalidatePath('/cart')
}
// stores/useCartStore.ts
export const useCartStore = create<CartState>((set) => ({
items: [],
addItem: async (item) => {
set((state) => ({ items: [...state.items, item] }))
// Sync to server
const items = useCartStore.getState().items
await syncCartToServer(items)
},
}))
// app/layout.tsx
import { CartStoreProvider } from '@/providers/CartStoreProvider'
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html>
<body>
<CartStoreProvider>
{children}
</CartStoreProvider>
</body>
</html>
)
}
// providers/CartStoreProvider.tsx
'use client'
import { useRef } from 'react'
import { useCartStore } from '@/stores/useCartStore'
export function CartStoreProvider({ children }: { children: ReactNode }) {
const initialized = useRef(false)
if (!initialized.current) {
// Initialize with server data if needed
useCartStore.setState({ items: [] })
initialized.current = true
}
return <>{children}</>
}
// ✅ Good: Single responsibility stores
const useAuthStore = create<AuthState>(...)
const useTodoStore = create<TodoState>(...)
const useUIStore = create<UIState>(...)
// ❌ Bad: God store
const useAppStore = create<AppState>(...)
// ✅ Good: Clear, verb-based actions
const useStore = create((set) => ({
addTodo: (text) => set(...),
removeTodo: (id) => set(...),
toggleTodo: (id) => set(...),
}))
// ❌ Bad: Vague or noun-based
const useStore = create((set) => ({
todo: (text) => set(...), // What does this do?
update: (id) => set(...), // Update what?
}))
// ✅ Good: Specific selectors
const user = useStore((state) => state.user)
const theme = useStore((state) => state.theme)
// ❌ Bad: Selecting entire store
const state = useStore() // Re-renders on any change
// ✅ Good: Explicit error state
interface State {
data: Data | null
loading: boolean
error: Error | null
fetchData: () => Promise<void>
}
// ❌ Bad: Silent failures
const fetchData = async () => {
try {
const data = await api.getData()
set({ data })
} catch (error) {
// Error silently ignored
}
}
interface ResourceState<T> {
data: T | null
loading: boolean
error: Error | null
status: 'idle' | 'loading' | 'success' | 'error'
}
function createResourceStore<T>() {
return create<ResourceState<T>>((set) => ({
data: null,
loading: false,
error: null,
status: 'idle',
fetch: async () => {
set({ loading: true, status: 'loading', error: null })
try {
const data = await fetchData()
set({ data, loading: false, status: 'success' })
} catch (error) {
set({ error, loading: false, status: 'error' })
}
},
}))
}
interface HistoryState<T> {
past: T[]
present: T
future: T[]
set: (state: T) => void
undo: () => void
redo: () => void
}
function createHistoryStore<T>(initialState: T) {
return create<HistoryState<T>>((set) => ({
past: [],
present: initialState,
future: [],
set: (newPresent) => set((state) => ({
past: [...state.past, state.present],
present: newPresent,
future: [],
})),
undo: () => set((state) => {
if (state.past.length === 0) return state
const previous = state.past[state.past.length - 1]
const newPast = state.past.slice(0, -1)
return {
past: newPast,
present: previous,
future: [state.present, ...state.future],
}
}),
redo: () => set((state) => {
if (state.future.length === 0) return state
const next = state.future[0]
const newFuture = state.future.slice(1)
return {
past: [...state.past, state.present],
present: next,
future: newFuture,
}
}),
}))
}
Zustand Advantages:
Redux Advantages:
Zustand Advantages:
Context Advantages:
Zustand Advantages:
Jotai Advantages:
When using Zustand, these skills enhance your workflow:
[Full documentation available in these skills if deployed in your bundle]
development
Optimize web performance using Core Web Vitals, modern patterns (View Transitions, Speculation Rules), and framework-specific techniques
development
Best practices for documenting APIs and code interfaces, eliminating redundant documentation guidance per agent.
development
Comprehensive API design patterns covering REST, GraphQL, gRPC, versioning, authentication, and modern API best practices
development
Visual verification workflow for UI changes to accelerate code review and catch ...