Zustand cheatsheet.
Setup
npm i zustand
import { create } from "zustand";
type CounterStore = {
count: number;
inc: () => void;
dec: () => void;
reset: () => void;
};
const useCounter = create<CounterStore>((set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
dec: () => set((s) => ({ count: s.count - 1 })),
reset: () => set({ count: 0 }),
}));
Usage
function Counter() {
const count = useCounter((s) => s.count);
const inc = useCounter((s) => s.inc);
return <button onClick={inc}>{count}</button>;
}
Always select what you need. Selecting the whole store re-renders on every change.
Multiple selections
import { useShallow } from "zustand/react/shallow";
const { count, inc } = useCounter(
useShallow((s) => ({ count: s.count, inc: s.inc }))
);
Without useShallow, the inline object identity changes every render → infinite re-renders.
Computed values
const doubled = useCounter((s) => s.count * 2);
Async actions
const useUsers = create<{
users: User[];
loading: boolean;
fetch: () => Promise<void>;
}>((set) => ({
users: [],
loading: false,
fetch: async () => {
set({ loading: true });
const users = await api.fetchUsers();
set({ users, loading: false });
},
}));
Outside React
useCounter.getState().count;
useCounter.setState({ count: 100 });
useCounter.subscribe((s, prev) => console.log(s.count));
Slices pattern
type AuthSlice = { user: User | null; login: (u: User) => void };
type CartSlice = { items: Item[]; add: (i: Item) => void };
const createAuthSlice: StateCreator<AuthSlice & CartSlice, [], [], AuthSlice> = (set) => ({
user: null,
login: (u) => set({ user: u }),
});
const createCartSlice: StateCreator<AuthSlice & CartSlice, [], [], CartSlice> = (set) => ({
items: [],
add: (i) => set((s) => ({ items: [...s.items, i] })),
});
const useStore = create<AuthSlice & CartSlice>()((...a) => ({
...createAuthSlice(...a),
...createCartSlice(...a),
}));
Devtools
import { devtools } from "zustand/middleware";
const useStore = create<State>()(
devtools((set) => ({ ... }), { name: "store" })
);
Inspect in Redux DevTools.
Persist
import { persist } from "zustand/middleware";
const useStore = create<State>()(
persist(
(set) => ({ count: 0, inc: () => set((s) => ({ count: s.count + 1 })) }),
{ name: "counter-storage" }
)
);
Saves to localStorage automatically.
Custom storage:
persist(..., {
name: "store",
storage: createJSONStorage(() => sessionStorage),
});
Partial persist:
persist(..., {
name: "store",
partialize: (s) => ({ count: s.count }), // only persist count
});
Immer
npm i immer
import { immer } from "zustand/middleware/immer";
const useStore = create<State>()(
immer((set) => ({
items: [],
add: (i) => set((s) => { s.items.push(i); }), // mutate
}))
);
subscribeWithSelector
import { subscribeWithSelector } from "zustand/middleware";
const useStore = create<State>()(
subscribeWithSelector((set) => ({ count: 0 }))
);
useStore.subscribe(
(s) => s.count,
(newCount, prev) => console.log("changed", prev, "→", newCount),
);
Combining middleware
import { persist, devtools, immer } from "zustand/middleware";
const useStore = create<State>()(
devtools(
persist(
immer((set) => ({ ... })),
{ name: "store" }
)
)
);
When vs Redux
Zustand wins for:
- Smaller codebase
- No provider boilerplate
- Direct calls outside components
Redux Toolkit wins for:
- Time-travel debugging
- Strict patterns enforced by team
- Mature plugin ecosystem (RTK Query)
For most apps today: Zustand.
When vs Context
Context: 1-time provider data (theme, auth). Re-renders all consumers on any change.
Zustand: any shared state, with selector-based subscriptions (only re-render on relevant change). Use for frequently-updating state.
Common mistakes
- Selecting
s(whole state) — re-renders on every change. - Object selector without
useShallow— re-renders forever. - Mutating state without
immer— Zustand doesn’t detect. - Calling
setStatein render — infinite loop. - Storing functions in persisted state — JSON can’t serialize.
Read this next
If you want my Zustand + slices template, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .