Choosing a State Library
TL;DR
First split your state: server state → a query library (TanStack Query / RTK Query), always. Then for the remaining client state, start as local as possible (useState) and lift only when sharing demands it. Reach for a global client store (Zustand/Jotai/Redux) when state is shared across distant components. Pick Redux/RTK specifically when client state is large and complex, the team needs enforced structure, or you want time-travel devtools and a rich middleware ecosystem — otherwise its boilerplate is a cost without payoff.
Interview Q&A
Q: What’s your decision framework?
A:
- Is it server data? → TanStack Query or RTK Query. Stop. (server_vs_client_state.md)
- Is it used by one component / a small subtree? →
useState/useReducer, lift to the nearest common parent if shared. - Shared across distant parts of the tree, low frequency? → Context (with care) or a small store.
- Shared widely, frequent updates, or complex? → Zustand/Jotai for light needs; Redux/RTK when you need structure and tooling.
The mistake is starting at step 4.
Q: Compare the main options.
A:
| Tool | Model | Boilerplate | Best for |
|---|---|---|---|
useState/useReducer |
local | none | component-local state |
| Context | top-down provide/consume | low | low-frequency global (theme, auth, locale) |
| Zustand | single store | low | shared client state, minimal ceremony |
| Jotai | atomic, bottom-up | low | many small/derived pieces |
| Valtio / MobX | proxy/observable | low | mutable mental model, fine-grained reactivity |
| Redux Toolkit | single store + conventions | medium | large/complex client state, big teams, devtools |
| TanStack / RTK Query | server cache | low | server state (not client state) |
Q: When is Redux the wrong choice?
A: When the app is small, the state is mostly server data, or the team wants minimal indirection. Symptoms of misuse: reducers full of isLoading/error flags (that’s server state — use a query lib), or a global store holding values only one component reads (that’s local state). “We use Redux because we always use Redux” isn’t a reason.
Q: What is state colocation and why does it matter?
A: Keep state as close to where it’s used as possible; only lift it when something else genuinely needs it. Colocated state is easier to reason about, re-renders a smaller subtree, and deletes cleanly with the component. Premature globalization creates coupling and unnecessary re-renders. The progression is: local → lifted → context/store, moved only when forced.
Q: How do you handle relational/normalized client state in Redux?
A: createEntityAdapter stores entities as { ids: [], entities: {} } (normalized, O(1) lookup) and generates CRUD reducers + memoized selectors (selectAll, selectById):
const adapter = createEntityAdapter<Todo>();
const slice = createSlice({
name: "todos",
initialState: adapter.getInitialState(),
reducers: { addTodo: adapter.addOne, updateTodo: adapter.updateOne, removeTodo: adapter.removeOne },
});
export const { selectAll, selectById } = adapter.getSelectors((s) => s.todos);
Normalization avoids duplicated nested data and the bugs that come from updating it in two places.
Q: Thunks, sagas, or listener middleware for side effects?
A:
- Thunks (built into RTK) — the default for simple async; just async functions dispatching actions.
- Listener middleware (
createListenerMiddleware) — the modern reactive option: run logic in response to dispatched actions or state changes, withcondition/takeLatest-style control, without saga’s generator overhead. The recommended replacement for most saga use cases. - Sagas — generator-based, powerful for complex orchestration/cancellation, but heavier; reach for them only when listener middleware isn’t enough.
Q: Where do Vue’s options fit?
A: Pinia is Vue’s official store (the Zustand-simplicity-with-Vue-reactivity option); Vuex is its legacy predecessor. The same server-vs-client split applies in Vue. See ../06_vue/10_pinia.md.
Gotchas / edge cases
- Don’t put server data in a client store — the most common architectural mistake; it reimplements caching badly.
- Context is not a state manager — it’s dependency injection; every consumer re-renders on value change. Split contexts or use a store for frequently-changing values (../05_react/context_performance.md).
- One global store for everything couples unrelated features and bloats re-renders; colocate.
- Migrations cost — picking a library is somewhat sticky; the cheapest first move (TanStack Query for server state) often removes 70% of perceived “state management” need before you choose a client store at all.
What a senior is expected to say
- “Split server vs client state first. Server → query library. Client → start local, lift only when shared, reach for a store when shared widely.”
- “Redux earns its boilerplate on large, complex client state with a big team and devtools needs; it’s the wrong tool for a small app or mostly-server-data app.”
- “Colocate state; Context is DI, not a performant store for hot values.”
- “For relational client data I normalize with
createEntityAdapter; for reactive side effects I prefer listener middleware over sagas.”
Cross-references
- The first split to make: server_vs_client_state.md
- Light client stores: zustand.md, jotai_valtio.md
- Redux Toolkit / RTK Query: redux/redux-toolkit.md, redux/rtk_query.md
- Vue equivalent: ../06_vue/10_pinia.md
- Context performance: ../05_react/context_performance.md
Further reading
- Redux — “Redux Essentials” and when to use Redux: https://redux.js.org/faq/general#when-should-i-use-redux
- Kent C. Dodds — “State Colocation will make your React app faster”: https://kentcdodds.com/blog/state-colocation-will-make-your-react-app-faster
- RTK —
createListenerMiddleware: https://redux-toolkit.js.org/api/createListenerMiddleware