cswork / libraryOpen practice workspace

Articles

Zustand Interview Questions: Selectors, Subscriptions, and Store Boundaries

Practice Zustand interview questions on selectors, useShallow, external subscriptions, immutable updates, slices, and request-safe store boundaries.

A task board has three components: Task, Filter, and Count. You edit one task title. Which components rerender? In a Zustand interview, “Zustand is lightweight” does not answer that question. You need to know what each component selects, how that selected value is compared, and which store instance it reads.

These Zustand interview questions use that small state-flow exercise to explain selectors, subscriptions, and store boundaries.

Evidence boundary: Official Zustand documentation supports the selector, useShallow, middleware, slice, and scoped-store behavior discussed here. The board, counts, and interview answer are illustrative reasoning exercises, not a claim about a particular company's round or a measured production app.

The short answer: select values and choose store lifetime

A Zustand store owns state and actions. React components read it through a hook or a scoped vanilla store with useStore. A selector chooses the value a component observes. If the selected output changes under the relevant equality rule, the component updates. Subscribing to the entire state object makes unrelated store changes relevant to that component.

There is a second design axis: the lifetime and ownership of the store instance. A module-level store can be appropriate for client-wide state in a single-page application. A store tied to one widget, document, test, user session, or server request should be scoped accordingly. Narrow selectors do not prevent two users from sharing a wrongly global server-side store. These are different problems with different repairs.

For the task board, suppose the store holds a task title, active filter, and task count. A title edit changes only the title. If Task selects title, Filter selects filter, and Count selects count, the ideal subscription behavior is that Task updates while the other selected values remain the same. If all three components select the whole store, all three may rerender from the new state object.

Work through the selector-rerender exercise

Write the state transition explicitly: title moves from “Write docs” to “Polish docs”; filter remains “all”; count remains 3. Then write each selected output before and after. A primitive selector for filter returns the same string both times; a primitive selector for count returns the same number. Task's selected title changes.

The answer depends on the actual component and React tree. A parent rerender can still rerender children for reasons unrelated to a store subscription. The exercise isolates updates caused by each Zustand selection; it is not a guarantee that React will render only one component in every application. Say what you are holding constant.

SubscriptionSelected output after title editExpected store-driven update
Task selects titleDifferent stringTask updates
Filter selects filterSame stringNo Filter update from this selection
Count selects countSame numberNo Count update from this selection
Every component selects whole stateNew state objectAll are exposed to the update

Do not memorize “selectors always optimize.” A selector that returns a fresh object or array on every calculation can produce a new reference even when its fields are unchanged. The default Object.is comparison sees different references. In current Zustand guidance, useShallow can provide stable selected outputs when the relevant fields are shallowly equal.

When does useShallow help?

Suppose a toolbar needs both filter and count. A selector returning a new object containing those two fields creates a fresh reference. When an unrelated task title changes, the fields remain equal but the object identity differs. A shallow comparison can recognize that its top-level selected fields are unchanged and avoid a store-driven rerender. A simpler alternative is often to select each primitive field separately.

Zustand's useShallow is not a general deep-comparison engine. If a nested object changes in place while its top-level reference remains the same, shallow comparison can miss a meaningful change. That is why immutable updates matter. Copy the nested levels that change, or use a controlled immutable-update helper; do not mutate an existing object and assume a selector will discover it.

For arrays of IDs, a shallow comparison can help when a derived list has the same members in the same order but is newly allocated. It will not declare two arrays equal if elements differ. If the array is huge or the derivation expensive, measure the work and consider whether a different state shape or memoized derived value would be clearer. Avoid adding a comparator to hide an imprecise selector.

Zustand's v5 migration guidance also warns that selectors returning unstable new references can cause more than a performance issue: some patterns can produce an infinite update loop. The safe response is to make the selected output stable, useShallow for a suitable shallow case, or restructure the selector. Do not report every render as “React being slow” without checking identity.

How are React selectors different from external subscriptions?

A component selector answers, “What state value does this component render from?” An external subscription answers, “When should this callback run outside React?” Zustand's subscribeWithSelector middleware supports subscribing to a selected part of a store and receiving callbacks when that part changes. That can be useful for synchronizing an imperative chart, analytics bridge, or browser API.

The lifecycle matters. A subscription returns a way to unsubscribe; attach it at a well-defined owner boundary and clean it up when the owner unmounts or is disposed. If a component creates a new listener every render without cleanup, one store update can fire several stale callbacks. If the callback closes over old props, it can write the wrong document or emit old telemetry.

Do not confuse subscription callbacks with business transactions. An external listener may run after a state update, but that does not make an API call and a store change atomic. For an async save, give the request an identity, handle cancellation or stale responses, and decide which result may update current state. A selector does not solve request ordering.

For the board, consider a browser tab title listener subscribed only to the active document name. A task title edit should not update the tab title. When the board widget is removed, the listener should be removed too. In a mock interview, explain both the selection and the cleanup; only one of them is a render-performance question.

Where should actions and immutable updates live?

Zustand actions commonly call set with a partial state update. The documented merge is shallow, not recursive. Updating a nested task record requires producing new objects at the levels whose references should change. A mutation such as changing task.title on an existing object and returning the same tasks map can leave selectors with unchanged references and inconsistent UI.

Prefer actions that encode the business transition: renameTask(id, title), setFilter(filter), and replaceBoard(boardId, data) are easier to audit than arbitrary components calling setState on internal structure. If multiple components must preserve the same invariant, centralize it in an action or domain function. Do not assume that every simple local input needs global state; a draft text field may remain component-local until save.

An async action can fetch data and then call set. Define the acceptance rule when two requests race: the later-started query may finish first, and an old response should not replace current board state. Store a request key or generation and compare it before committing, or use a dedicated server-state tool when caching, invalidation, and retries dominate. Zustand is not a substitute for every remote-data policy.

If a save fails, decide whether to roll back optimistic state, keep an unsaved draft, or mark the record for retry. State transitions should be testable without a browser: start with a small store, invoke the action, and assert the selected outputs and invariants. A rendering test can then check that components display the expected result.

When should you split a store into slices?

The official slice pattern composes state creators so a larger store can be organized by concern. It helps with maintainability when one store's actions and fields become unwieldy. Slices within a combined store are not automatically isolated store instances: if they are spread into one bound store, they still share that store's state and update mechanism.

Use slices when related domains benefit from separate modules but still need one shared store. Use genuinely separate stores when lifetime, ownership, security, or testing isolation differs. A board editor may use one scoped store per open board; a theme preference may live in a client-wide store. Placing both in “slices” of a single module-level store does not give the board a per-instance lifetime.

Zustand's slice guidance advises applying middleware at the combined store rather than independently inside every slice. Explain why in an interview: persistence or instrumentation should wrap the final state boundary consistently. Then test the combined actions, especially when one slice reads another's values.

Do not split based on file length alone. First identify who owns data, who reads it, how it resets, and whether separate instances can coexist. Two boards open side by side make a useful test: if editing one changes the other's selected task, the boundary is wrong even if selectors are narrow.

What changes under SSR or multiple requests?

On a server that handles multiple users, a module-global mutable store can leak state between requests. Zustand's Next.js guidance recommends a per-request store and warns about hydration consistency. This is a state ownership issue, not a rerender optimization. A client-side singleton that is fine in one browser tab is not automatically safe as server module state.

A store factory can create a vanilla store for each request or widget. A React context provider can pass the right instance to its subtree, with useStore selecting values from that instance. Initialize server and client consistently for hydration. Do not read or mutate a client store from a React Server Component simply because the import succeeds.

The interview prompt may not involve Next.js. Do not force SSR into every Zustand answer. Bring it up when the app renders on a shared server, needs multiple independent instances, or must reset state on navigation. Otherwise state the browser-only assumption and focus on selectors and actions.

A concise interview answer

“I would model the board with explicit actions and immutable updates. Components select only the values they render; a title edit should change the Task selection but not unchanged Filter or Count selections, apart from parent-driven renders. For a derived object or array, I would check reference stability and useShallow only when shallow equality matches the intended semantics. External subscribeWithSelector listeners need a clear owner and cleanup. I would choose a module-level or scoped store based on lifetime and request isolation, not just convenience; a shared server request must not reuse another user's mutable store.”

The best preparation is to predict and test one concrete update, not to memorize a list of middleware names. Start with Design State for a Three-Column Work Board and defend the selector and store boundary for each component.

Sources and Further Reading

Research checked September 21, 2026. Confirm your installed Zustand and React versions before using a version-specific API or migration rule.