Zustand Cheat Sheet
A quick reference for Zustand's minimal store API, selectors, and middleware like persist and devtools for React state.
Creating a Store
A store is just a hook created from a state initializer function.
import { create } from 'zustand';const useCounterStore = create((set, get) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), reset: () => set({ count: 0 }), getDoubled: () => get().count * 2,}));
Using the Store & Selectors
Selecting only the state a component needs, including multi-field selection.
function Counter() { const count = useCounterStore((state) => state.count); const increment = useCounterStore((state) => state.increment); return <button onClick={increment}>Count: {count}</button>;}// select multiple fields with a shallow comparisonimport { useShallow } from 'zustand/react/shallow';const { count, increment } = useCounterStore( useShallow((state) => ({ count: state.count, increment: state.increment })));
Middleware: persist & devtools
Persisting store state to localStorage and connecting to Redux DevTools.
import { create } from 'zustand';import { persist, devtools } from 'zustand/middleware';const useStore = create( devtools( persist( (set) => ({ theme: 'light', toggleTheme: () => set((s) => ({ theme: s.theme === 'light' ? 'dark' : 'light' })), }), { name: 'theme-storage' } // localStorage key ) ));
Core API
The handful of functions that make up Zustand's surface area.
- create- creates a store hook from a state initializer function
- set- updates state with a shallow merge by default; pass a function to base it on the previous state
- get- reads current state from inside actions without creating a subscription
- persist- middleware that saves store state to localStorage (or another storage) automatically
- devtools- middleware that connects the store to the Redux DevTools extension
- subscribe- subscribes to store changes outside of React components
- useShallow- selector helper for picking multiple fields while avoiding unnecessary re-renders
Slicing the Store into Modules
Split a large store into independent slices and combine them with a single create() call so actions can still read/write across slices.
const createUserSlice = (set, get) => ({ user: null, setUser: (user) => set({ user }),});const createCartSlice = (set, get) => ({ items: [], addItem: (item) => set((state) => ({ items: [...state.items, item] })), checkout: () => { // slices can call across each other via get() const { user } = get(); if (!user) throw new Error('Must be logged in to checkout'); },});const useStore = create((...a) => ({ ...createUserSlice(...a), ...createCartSlice(...a),}));
Immer Middleware for Nested Updates
Write mutating-looking draft updates for deeply nested state without manual spread chains.
import { create } from 'zustand';import { immer } from 'zustand/middleware/immer';const useStore = create( immer((set) => ({ todos: { byId: {}, allIds: [] }, toggleTodo: (id) => set((state) => { // mutate the draft directly; immer produces the new immutable state state.todos.byId[id].done = !state.todos.byId[id].done; }), })));
Transient Updates & subscribeWithSelector
Read or react to store changes outside the React render cycle, useful for canvas/animation loops or imperative side effects.
import { create } from 'zustand';import { subscribeWithSelector } from 'zustand/middleware';const useStore = create( subscribeWithSelector((set) => ({ x: 0, y: 0 })));// non-reactive read: does not subscribe the calling componentconst { x } = useStore.getState();// fires only when `x` actually changes, with a custom equality fnconst unsubscribe = useStore.subscribe( (state) => state.x, (x, prevX) => console.log('x changed', prevX, '->', x), { equalityFn: Object.is, fireImmediately: false });// imperative write, e.g. inside a requestAnimationFrame loopuseStore.setState((s) => ({ x: s.x + 1 }));unsubscribe();
Vanilla Store + React Context (per-instance stores)
Use createStore from zustand/vanilla with a Context provider to give each component tree its own isolated store instance, avoiding a global singleton and SSR state leakage.
import { createStore, useStore } from 'zustand';import { createContext, useContext, useRef } from 'react';const createCounterStore = (initial = 0) => createStore((set) => ({ count: initial, increment: () => set((s) => ({ count: s.count + 1 })), }));const CounterContext = createContext(null);function CounterProvider({ initial, children }) { const storeRef = useRef(); if (!storeRef.current) storeRef.current = createCounterStore(initial); return ( <CounterContext.Provider value={storeRef.current}> {children} </CounterContext.Provider> );}function useCounter(selector) { const store = useContext(CounterContext); return useStore(store, selector);}
Advanced API Surface
Lesser-known exports and options that matter once a store grows beyond a toy example.
- combine- middleware that infers state types from an initial state object, reducing boilerplate in TypeScript stores
- createStore (vanilla)- creates a framework-agnostic store object with getState/setState/subscribe, usable outside React
- partialize- persist option that selects which fields of state get written to storage
- onRehydrateStorage- persist callback invoked once cached state has been restored, useful for a 'hydrated' flag
- storeApi.destroy- tears down all subscribers on a vanilla store instance (legacy API, mostly relevant for manual cleanup)
- StateCreator<T>- the TypeScript type for the (set, get, api) => state function passed to create, used when typing slices
- redux middleware- wraps a store so it updates via a single dispatch(action) + reducer, for teams migrating from Redux
- skipHydration- persist option to defer rehydration until you manually call rehydrate(), needed for SSR frameworks
Selecting the whole state, e.g. useStore(), re-renders the component on every store change -- always select only the specific field(s) a component needs, e.g. useStore(state => state.count), to keep re-renders minimal.