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 tolocalStorage/AsyncStoragewith versioning + migrations.immer— write “mutating” updates safely (set((s) => { s.items.push(x) })).devtools— Redux DevTools integration (time-travel, action log).subscribeWithSelector— fine-grainedsubscribe.
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
useShallowre-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.
setshallow-merges top level only — nested updates need spreads or theimmermiddleware.persisthydration is async-ish — guard against rendering with pre-hydration state (use theonRehydrateStorage/hasHydratedflag) 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
useShallowis the classic re-render bug.” - “Slices pattern organizes a single store by feature;
persist/immer/devtoolsmiddleware cover the common needs.” - “I can call
getState/setState/subscribeoutside 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
- Server vs client state (what belongs in Zustand): server_vs_client_state.md
- Atom/proxy alternatives: jotai_valtio.md
- Picking among the options: choosing_a_state_library.md
- Context performance (the thing Zustand sidesteps): ../05_react/context_performance.md
Further reading
- Zustand docs: https://zustand.docs.pmnd.rs/
- Zustand — Preventing rerenders with
useShallow: https://zustand.docs.pmnd.rs/guides/prevent-rerenders-with-use-shallow