Zustand

Updated 5 interview angles 4 min read source
On this page11
  1. What does a basic Zustand store look like?
  2. How does Zustand control re-renders?
  3. How do you scale a store — the slices pattern?
  4. What middleware matters?
  5. How do you use the store outside React?
  6. What are “transient updates” and why use them?
  7. Zustand vs Redux?
  8. Gotchas
  9. Related
  10. Further reading
  11. Interview angle

Zustand

Zustand is a minimal client-state store: a hook created by create(), no provider, no boilerplate, and the store lives outside React so you can read/write it anywhere. You subscribe to slices with selectors, and a component re-renders only when its selected slice changes. It’s the popular middle ground between “just useState/Context” and “full Redux.”

What does a basic Zustand store look like?

ts
import { create } from "zustand";

const useStore = create<{ count: number; inc: () => void }>((set) => ({
  count: 0,
  inc: () => set((s) => ({ count: s.count + 1 })),
}));

function Counter() {
  // subscribe to just `count`
  const count = useStore((s) => s.count);
  const inc = useStore((s) => s.inc);
  return <button onClick={inc}>{count}</button>;
}

No <Provider>, no reducers, no action types. set does a shallow merge by default.

How does Zustand control re-renders?

Via the selector. useStore((s) => s.count) re-renders only when count’s reference changes. Selecting the whole store (useStore()) re-renders on any change — avoid it. For selecting multiple fields, return a stable shape and pass an equality fn:

ts
import { useShallow } from "zustand/react/shallow";
const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })));

Without useShallow, returning a fresh object each render defeats memoization and re-renders every time. This is the #1 Zustand gotcha.

How do you scale a store — the slices pattern?

Compose multiple slice creators into one store:

ts
const createCartSlice = (set) => ({ items: [], add: (i) => set((s) => ({ items: [...s.items, i] })) });
const createUiSlice = (set) => ({ modalOpen: false, toggle: () => set((s) => ({ modalOpen: !s.modalOpen })) });

const useStore = create((...a) => ({ ...createCartSlice(...a), ...createUiSlice(...a) }));

Keeps a single store but organizes by feature — the Zustand answer to Redux’s combineReducers.

What middleware matters?

  • persist — save to localStorage/AsyncStorage with versioning + migrations.
  • immer — write “mutating” updates safely (set((s) => { s.items.push(x) })).
  • devtools — Redux DevTools integration (time-travel, action log).
  • subscribeWithSelector — fine-grained subscribe.
ts
const useStore = create(persist(immer((set) => ({ /* ... */ })), { name: "cart", version: 1 }));

How do you use the store outside React?

The created hook carries static methods — useful in event handlers, sagas, tests, or non-React code:

ts
useStore.getState().inc();          // read/call without a component
useStore.setState({ count: 0 });    // write directly
const unsub = useStore.subscribe((s) => console.log(s.count));

This is a real advantage over Context, which is React-only.

What are “transient updates” and why use them?

For high-frequency values (mouse position, scroll) you can subscribe imperatively and write to a ref instead of triggering re-renders:

ts
useEffect(() => useStore.subscribe((s) => { boxRef.current.style.transform = `translateX(${s.x}px)`; }), []);

The component never re-renders; you mutate the DOM directly. Avoids render storms.

Zustand vs Redux?

Zustand: less boilerplate, no provider, store callable outside React, tiny bundle. Redux/RTK: stronger conventions, a richer middleware/devtools ecosystem, structured large-team flows, and RTK Query for server state. For most apps that only need some shared client state, Zustand wins on simplicity; Redux earns its weight when client state is large and the team needs enforced structure.

Gotchas

  • Returning a new object/array from a selector without useShallow re-renders every time — selectors must return stable references or use shallow equality.
  • SSR / Next.js: a module-level store is shared across requests on the server — leaking one user’s state to another. Create the store per request (e.g., via a context-provided factory) for SSR.
  • set shallow-merges top level only — nested updates need spreads or the immer middleware.
  • persist hydration is async-ish — guard against rendering with pre-hydration state (use the onRehydrateStorage/hasHydrated flag) to avoid hydration mismatches.
  • It’s client state, not server state — don’t fetch-and-store API data in Zustand; use a query library (Server State vs Client State).

Further reading

Interview angle 5

  • “What is Zustand’s model?” - a small store living outside React, with components subscribing through selectors. The selector decides what re-renders, so a component that selects one field ignores every other change to the store.
  • “What is the classic Zustand re-render bug?” - a selector returning a fresh object or array each call, so the reference always differs and the component re-renders on every store update. useShallow around the selector compares the contents instead of the reference.
  • “How do you organise a large store?” - the slices pattern: one store composed from per-feature slice creators, so features stay separable without the overhead of several stores. persist, immer and devtools middleware cover most of what you would otherwise write.
  • “What can you do outside React?” - getState, setState and subscribe. That makes the store usable from event handlers, workers and plain modules, and a transient subscription lets you react to a high-frequency value - a cursor position, a scroll offset - without re-rendering at all.
  • “What breaks under SSR?” - a module-scope store shared across requests, so one user’s state is visible to the next. Create the store per request and pass it through a provider on the server; a single shared instance is only safe in the browser.