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 setState in 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 .