A React app now has a dozen Zustand stores owned by several teams; when does Zustand's missing structure start to hurt compared with Redux Toolkit, and what would you do about it?
answer
- functions versus described events
- one log versus many stores
- who changed this field
- stores reaching into stores
- conventions before migrations
basics
~20 sZustand updates are plain functions calling set across independent stores, so nothing records or routes every change. That hurts when teams need one change log, shared middleware or cross-store rules; tighten conventions first, and move to Redux Toolkit only if those needs dominate.
solid answer
~50 sThe concrete mechanical difference is how a change happens. In Zustand an action is a function that calls `set`; in Redux Toolkit every change is a dispatched action object that flows through middleware into reducers of one store. At small scale Zustand's version is simply less code. With a dozen stores and several teams, the cost shows up as: no single ordered log of what changed and why, cross-cutting behaviour (logging, analytics, undo) having to be attached store by store, and actions in one store quietly calling another store's `getState()`, which hides coupling. I would first measure where it hurts, then tighten conventions: domain stores built from slices, actions only inside stores, `devtools` with named actions, clear store ownership, and no cross-store writes from components. Migrating to Redux Toolkit is justified when a central event stream or its middleware model is a recurring requirement, not because the store count grew.
go deeper
Recall the basic contrast: Zustand actions are functions calling set in small stores, while Redux Toolkit routes named action objects through reducers of one store.
Explain what each model gives you for free: Zustand less ceremony, Redux Toolkit a single ordered pipeline that middleware and devtools can observe.
Diagnose the real pain in a codebase: untraceable updates, cross-store getState and setState calls, and per-store middleware drift, and fix them with conventions.
Own the decision with evidence: measure the failure modes, run a convention trial, and migrate incrementally only when a central event stream is a recurring requirement.
## The mechanical difference that matters Comparisons between Zustand and Redux often turn into feature lists. The difference that decides this question is narrower: **how a state change is expressed**. | | Zustand 5 | Redux Toolkit | |---|---|---| | Unit of change | a function in the store that calls `set` | a plain action object passed to `dispatch` | | Where logic lives | inside that function | in reducers generated by `createSlice` | | Number of stores | as many as you call `create` | one store, combined from slice reducers | | Interception point | per-store middleware wrapping `set` | store-wide middleware seeing every action | | Change log | only if each store uses `devtools` | every action is inspectable in Redux DevTools | In Zustand, `addItem` is just a function; nothing requires that it have a name, that it be the only way to change `items`, or that anyone outside the store can observe it except by subscribing to state. In Redux Toolkit, the action object *is* the record of intent, and because everything flows through one `dispatch`, cross-cutting code sees every change in order. ## Where the missing structure starts to hurt It is rarely the store count itself. The signals are: 1. **"Who changed this?" questions take too long.** Several stores update in response to one user action, and there is no single ordered record of which function ran. Zustand's `devtools` middleware helps per store, but only if every store uses it and actions are named — otherwise it guesses a name from the call stack or records `anonymous`. 2. **Cross-store coupling.** An action in the checkout store reads `useCartStore.getState()` and writes `usePricingStore.setState(...)`. Each call is legal, but the dependency graph lives in function bodies, not in anything a reviewer can see. 3. **Cross-cutting requirements repeat.** Audit logging, analytics on state transitions or undo must be added to every store, with every team wiring its own middleware stack in its own order. 4. **Inconsistent patterns.** Some teams put actions in the store, others call `setState` from components; some merge nested objects by hand, others use the `immer` middleware. If none of these recur, a dozen small, focused stores is a healthy design, not a problem. ## What to do before migrating Most of the pain is convention debt, and Zustand gives enough tools to pay it down: - **Domain stores built from slices.** Combine related slices into one bound store with the slices pattern, and apply middleware once on the combined store, as the docs advise. - **Actions only inside stores.** Components select and call actions; they never call `setState` directly. This makes each store's API explicit. - **Named actions in `devtools`.** Pass the name as the third argument to `set` so the timeline reads like an event log in development. - **Ownership and boundaries.** One owning team per store, and cross-store writes only through the owning store's actions. - **A shared middleware recipe.** One helper that applies the same stack in the recommended order, `devtools` outermost. If a team wants reducer-style updates for one complex store, Zustand's `redux` middleware gives that store a reducer and a `dispatch` without changing libraries. ## When Redux Toolkit is the better call Migration earns its cost when the *requirements* are about the event stream itself: many features must react to the same user intents, a single time-ordered log is a product or compliance need, or teams already lean on store-wide middleware. Then Redux Toolkit's model — every change is a named action through one pipeline — is doing work Zustand would have to imitate. ## How I would run the decision - Measure first: count incidents and review comments caused by cross-store coupling or untraceable updates over a few months. - Try conventions for a fixed period and re-measure. - If you migrate, go store by store behind the same component-facing hooks, so feature code does not change twice. - Keep Zustand for small, local, fast-moving state even then; the two coexist fine. The weak answer is "Redux scales, Zustand does not". The strong answer names the mechanism, the symptoms that prove it matters in *this* codebase, and the cheapest intervention that addresses them.
- How do you make Zustand's devtools timeline readable enough to answer who changed a field?Apply `devtools` to every store (outermost in the middleware stack), give each store a `name` option, and pass an action name as the third argument to `set`, for example `set(next, undefined, 'cart/addItem')`. Without a name, devtools tries to infer one from the call stack and otherwise records `anonymous`, which is unhelpful across many stores.
- One Zustand action reads another store with getState() and writes a third with setState(). Is that wrong?It is legal and sometimes pragmatic, but it hides coupling in function bodies and lets any store write any other. At team scale, prefer calling the owning store's action instead of its `setState`, or merge tightly-coupled state into one store built from slices so the dependency is visible in one place.
saying these in an interview costs you the question
- Zustand cannot be used in large apps; past a few stores you must switch to Redux.
- Zustand and Redux are the same model; the choice is only about boilerplate.
- Adding devtools to one store gives a complete log of changes across all stores.
- More stores is the problem, so the fix is always a single giant Zustand store.
- A migration should start by rewriting every component that reads state.