Skip to Content
Grab N GoState (Zustand)

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().count
Subscribe
// 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 immer
Shallow 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-selected
Scoped 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