frontend / state managers / choosing_a_state_library.md

Choosing a State Library

4 min read source

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:

  1. Is it server data? → TanStack Query or RTK Query. Stop. (server_vs_client_state.md)
  2. Is it used by one component / a small subtree?useState/useReducer, lift to the nearest common parent if shared.
  3. Shared across distant parts of the tree, low frequency? → Context (with care) or a small store.
  4. 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, with condition/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

Further reading