State (Zustand)
Targeting Zustand 5.x
Syntax
Create a Store
import { create } from 'zustand'
interface CountStore {
count: number
inc: () => void
}
const useCount = create<CountStore>((set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
}))Use in Component
function Counter() {
const count = useCount((s) => s.count)
const inc = useCount((s) => s.inc)
return <button onClick={inc}>{count}</button>
}Set State
// partial update — merges with existing state
set({ count: 10 })
// updater function — access previous state
set((s) => ({ count: s.count + 1 }))
// replace entire state (instead of merge)
set({ count: 0 }, true)Get State
// inside an action — use get()
const useStore = create<Store>((set, get) => ({
items: [],
total: () => get().items.length,
}))
// outside React — read directly
const count = useCount.getState().countSubscribe
// listen to all changes outside React
const unsub = useCount.subscribe((state) => {
console.log('count is now', state.count)
})
// cleanup
unsub()Selectors
// select a single field — only re-renders when it changes
const name = useStore((s) => s.name)
// select derived value
const total = useStore((s) => s.items.length)
// grab everything (re-renders on any change)
const state = useStore()Beginner Patterns
Multiple Actions
interface TodoStore {
todos: string[]
add: (todo: string) => void
remove: (i: number) => void
clear: () => void
}
const useTodos = create<TodoStore>((set) => ({
todos: [],
add: (todo) => set((s) => ({ todos: [...s.todos, todo] })),
remove: (i) => set((s) => ({ todos: s.todos.filter((_, j) => j !== i) })),
clear: () => set({ todos: [] }),
}))Object State
interface UserStore {
user: { name: string; email: string } | null
setUser: (user: UserStore['user']) => void
logout: () => void
}
const useUser = create<UserStore>((set) => ({
user: null,
setUser: (user) => set({ user }),
logout: () => set({ user: null }),
}))Toggle Pattern
interface UIStore {
sidebarOpen: boolean
toggleSidebar: () => void
}
const useUI = create<UIStore>((set) => ({
sidebarOpen: false,
toggleSidebar: () => set((s) => ({
sidebarOpen: !s.sidebarOpen,
})),
}))Async Action
interface PostStore {
posts: Post[]
loading: boolean
fetch: () => Promise<void>
}
const usePosts = create<PostStore>((set) => ({
posts: [],
loading: false,
fetch: async () => {
set({ loading: true })
const res = await fetch('/api/posts')
const posts = await res.json()
set({ posts, loading: false })
},
}))Reset Store
const initialState = { count: 0, name: '' }
interface Store {
count: number
name: string
reset: () => void
}
const useStore = create<Store>((set) => ({
...initialState,
reset: () => set(initialState),
}))Actions Outside React
// call actions without a hook
useTodos.getState().add('Buy milk')
// useful in event handlers, utils, tests
const { user, logout } = useUser.getState()
if (!user) logout()Intermediate Patterns
Slices
interface AuthSlice {
token: string | null
login: (token: string) => void
}
interface UISlice {
theme: 'light' | 'dark'
toggleTheme: () => void
}
type Store = AuthSlice & UISlice
const useStore = create<Store>((set) => ({
token: null,
login: (token) => set({ token }),
theme: 'dark',
toggleTheme: () => set((s) => ({
theme: s.theme === 'dark' ? 'light' : 'dark',
})),
}))Immer Middleware
import { immer } from 'zustand/middleware/immer'
const useStore = create<Store>()(
immer((set) => ({
items: [{ id: 1, done: false }],
toggle: (id: number) => set((s) => {
// mutate directly — immer handles immutability
const item = s.items.find((i) => i.id === id)
if (item) item.done = !item.done
}),
}))
)Persist Middleware
import { persist } from 'zustand/middleware'
const useSettings = create<SettingsStore>()(
persist(
(set) => ({
theme: 'dark',
setTheme: (theme: string) => set({ theme }),
}),
{ name: 'settings' } // localStorage key
)
)Devtools
import { devtools } from 'zustand/middleware'
const useStore = create<Store>()(
devtools(
(set) => ({
count: 0,
inc: () => set(
(s) => ({ count: s.count + 1 }),
undefined,
'inc' // action name in devtools
),
}),
{ name: 'CountStore' }
)
)Combine Middleware
import { devtools, persist } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'
const useStore = create<Store>()(
devtools(
persist(
immer((set) => ({
// store definition
})),
{ name: 'store' }
)
)
)
// order: devtools wraps persist wraps immerShallow Equality
import { useShallow } from 'zustand/react/shallow'
// without — new object every render, always re-renders
const { name, age } = useStore((s) => ({
name: s.name,
age: s.age,
}))
// with useShallow — only re-renders if values change
const { name, age } = useStore(
useShallow((s) => ({ name: s.name, age: s.age }))
)Advanced Patterns
Computed Values
interface CartStore {
items: { price: number; qty: number }[]
addItem: (item: CartStore['items'][0]) => void
total: () => number
}
const useCart = create<CartStore>((set, get) => ({
items: [],
addItem: (item) => set((s) => ({
items: [...s.items, item],
})),
total: () => get().items.reduce(
(sum, i) => sum + i.price * i.qty, 0
),
}))Transient Updates
// subscribe without causing re-renders
// useful for animations, cursors, scroll positions
interface MouseStore {
x: number
y: number
setPos: (x: number, y: number) => void
}
const useMouse = create<MouseStore>((set) => ({
x: 0, y: 0,
setPos: (x, y) => set({ x, y }),
}))
// read in RAF loop — no React render
function animate() {
const { x, y } = useMouse.getState()
// move element to x, y
requestAnimationFrame(animate)
}createSelectors Helper
// auto-generate hooks for each field
function createSelectors<T extends object>(
store: UseBoundStore<StoreApi<T>>
) {
const selectors = {} as {
[K in keyof T]: () => T[K]
}
for (const k of Object.keys(store.getState()) as (keyof T)[]) {
selectors[k] = () => store((s) => s[k])
}
return selectors
}
const use = createSelectors(useStore)
const count = use.count() // auto-selectedScoped Stores
import { createStore, useStore } from 'zustand'
import { createContext, useContext, useRef } from 'react'
// factory for per-component instances
const StoreContext = createContext<StoreApi<Store> | null>(null)
function StoreProvider({ children }: { children: React.ReactNode }) {
const ref = useRef(createStore<Store>(() => ({
count: 0,
})))
return (
<StoreContext value={ref.current}>
{children}
</StoreContext>
)
}Selective Persist
import { persist } from 'zustand/middleware'
const useAuth = create<AuthStore>()(
persist(
(set) => ({
token: null,
user: null,
tempData: null,
login: (token, user) => set({ token, user }),
}),
{
name: 'auth',
// only persist token and user, skip tempData
partialize: (s) => ({ token: s.token, user: s.user }),
}
)
)Subscribe with Selector
import { subscribeWithSelector } from 'zustand/middleware'
// middleware enables selector-based subscribe
const useStore = create<Store>()(
subscribeWithSelector((set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
}))
)
const unsub = useStore.subscribe(
(state) => state.count,
(count, prev) => console.log(`${prev} → ${count}`)
)Last updated on