Zustand

4 min read source

Zustand

TL;DR

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.”

Interview Q&A

Q: What does a basic Zustand store look like?

A:

import { create } from "zustand";

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

function Counter() {
  const count = useStore((s) => s.count);   // subscribe to just `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.

Q: How does Zustand control re-renders?

A: 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:

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.

Q: How do you scale a store — the slices pattern?

A: Compose multiple slice creators into one store:

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.

Q: What middleware matters?

A:

  • 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.
const useStore = create(persist(immer((set) => ({ /* ... */ })), { name: "cart", version: 1 }));

Q: How do you use the store outside React?

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

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.

Q: What are “transient updates” and why use them?

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

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.

Q: Zustand vs Redux?

A: 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 / edge cases

  • 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_vs_client_state.md).

What a senior is expected to say

  • “Zustand is a tiny store outside React; selectors decide re-renders, and returning fresh objects without useShallow is the classic re-render bug.”
  • “Slices pattern organizes a single store by feature; persist/immer/devtools middleware cover the common needs.”
  • “I can call getState/setState/subscribe outside React, and use transient subscriptions for high-frequency values without re-rendering.”
  • “On SSR I create the store per request to avoid cross-request leakage.”

Cross-references

Further reading