I stopped reaching for a global state library
After ten years and three production stores (Redux, NgRx, then TanStack Query), most of what I used to call "state" turned out to be server data with a bad caching strategy. Here's the decision tree that replaced the default reach.
For most of my career, “state” meant Redux or NgRx. Spin up the store, write the reducers, draw the action diagram. Default for every project, regardless of whether the project actually needed one.
Now I almost never reach for a global state library. Not because the libraries are bad — they’re fine — but because the world around them changed and the default didn’t.
The thesis
Most of what we used to call “state” was server data with a bad caching strategy. The store wasn’t holding state; it was holding the result of a fetch and pretending that’s the same thing.
Once you have a server-cache library that knows about staleness, retries, dedupe, and optimistic updates — about 70% of typical Redux usage evaporates. There was nothing to manage. The store was a worse Map<URL, JSON> with extra ceremony.
The decision tree that replaced “use Redux”
When I’m reaching for state today, I ask, in order:
- Is this server data? → TanStack Query / SWR. Cache, don’t store.
- Is this filter / pagination / modal-open / tab-selection? → URL search params. Free, persistent, shareable, restorable. Every modern router has type-safe versions now.
- Is this UI ephemera? Hover, focus, “did the confirm button get clicked yet” →
useStatein the component that owns it. - Is this complex but local — a multi-step form, an editor draft? → Reducer per island.
useReducerin the subtree that owns the workflow. - Is this genuinely shared client state across the app — and not server data, not URL state, not local UI? → Tiny store. Zustand, 4kb, no ceremony.
- Does none of the above fit? → I haven’t hit one in 18 months. If I do, that’s when Redux earns its weight.
The default branch used to be #5. Now it’s almost never reached. That’s the entire reversal.
What’s actually left for a global store
Real cases where I’d still install a small store today:
- Wizards / multi-step flows where four screens write into one shared draft and abandoning resets everything.
- Editor-shaped problems — Slate, Lexical, Yjs. Document state is genuinely tree-like, genuinely cross-tree, and the editor library usually wraps a store of its own anyway.
- Cross-tree client-only state that doesn’t live on the server and shouldn’t live in the URL — rare, but real. Notification stacks. Optimistic-only client mutations awaiting reconciliation.
Notice none of those are list/detail data. None of them are filters. None of them are auth state — that’s identity, not state, and Context handles it.
What changed in the world
This isn’t Redux is bad. Redux was the right answer for 2016. The world it was designed for had:
- No standard fetching primitive; every team rolled their own.
- No HTTP cache on the client beyond browser defaults.
- No Suspense, no transitions, no RSC.
- “State” meaningfully included caching by definition — there was nowhere else to put it.
Server-cache libraries, type-safe URL state, RSC, and form actions each carved a slice off Redux’s territory. The slice that’s left is small enough that a 400-line store is overkill for it.
The trap I see most often
Two failure modes, both common.
The cosmetic reversal. Teams that have moved on from Redux but reproduce the same shape in a “lighter” library. Same boilerplate, same action/reducer ceremony, just with set() instead of dispatch(). That’s not a reversal — that’s the same default with new syntax. The reversal isn’t the library; it’s the decision tree.
The store as a band-aid for the API. A normalised entity store, custom invalidation rules, and “stale” flags everywhere — all because the API doesn’t speak HTTP caching properly. ETags, Cache-Control, conditional requests, 304 Not Modified exist for a reason. If the bandwidth and freshness problems are upstream, no client store is going to fix them. It’ll just hide them behind another abstraction the team has to maintain.
Good API design plus a competent client-side cache solves most of what teams reach for Redux to solve. A state library should never be the place where your API’s caching strategy lives. That’s the API’s job; if it’s not doing it, fix that first. The 304 response was put in the spec for a reason; using a 200 every time and patching it client-side with a store is paying the bandwidth bill twice.
What might reverse this entry
Two scenarios where I’d update or reverse:
- Signals everywhere. If every framework converges on signals as a primitive (Solid-style), even the tiny stores start to feel like ceremony around a one-line
signal()call. The “tiny store” branch collapses. - RSC + server actions cover the rest. If RSC matures enough that even multi-step wizards live as server-driven flows, the wizard branch goes too. That’d be a
reversedcallout.
Either is plausible by 2027. Until then, the decision tree above is what I’d hand a junior engineer asking “should I install Redux.”