You Probably Don't Need Redux. You Need Better Data Fetching.

A large portion of the state management complexity in React apps isn't really state management — it's server cache management pretending to be state.
For a long time, Redux was the default answer to "how do we manage state in this React app?" Not because everyone had carefully evaluated the options, but because it was there first, had the most Stack Overflow answers, and came up in every tutorial. So teams adopted it, built elaborate slices and reducers, and ended up with a store that was half application state and half server data that should never have been there in the first place.
The distinction matters: there are two fundamentally different kinds of state in most web apps. Client state is what the user is doing right now — which modal is open, what's in the form before submission, whether a sidebar is collapsed. Server state is data that lives on the server, that needs to be fetched, cached, potentially shared between components, and kept fresh. These two categories have almost nothing in common. They have different lifecycles, different invalidation strategies, different synchronization requirements. Treating them the same way is the root cause of a lot of unnecessary complexity.
Most of what teams put in Redux is server state. And server state doesn't belong in a client state manager — it belongs in a cache.
What a cache gives you that a store doesn't
When you fetch a list of users and store them in Redux, you've created a snapshot. That snapshot was accurate at the moment of the fetch. After that, it drifts. You need to decide when to re-fetch. You need to decide how to handle loading states. You need to decide what happens when the same data is needed in two different parts of the component tree simultaneously. And you need to build all of that infrastructure yourself.
React Query (and its alternatives like SWR) solves this by treating server data as a cache, not a store. The cache knows when data is stale. It handles deduplication — if five components request the same query key at the same time, one network request goes out. It handles background refetching so data stays fresh without any manual intervention. It handles optimistic updates and rollbacks. All of this comes out of the box.
The query key as a dependency graph
One of the most powerful (and underused) features of React Query is the query key. It's not just a cache identifier — it's a dependency declaration. If your users query depends on a filter or a search term, including that value in the key means the cache treats different values as different queries. No manual reset. No useEffect cleanup logic. The cache just works.
This pattern also makes prefetching straightforward. If you can predict what query a user is likely to run next — clicking into a detail view, navigating to another route — you can warm the cache before they get there. The perceived performance difference is significant.
- Automatic background refetching on window focus and network reconnect
- Request deduplication across concurrent components
- Optimistic updates with automatic rollback on error
- Infinite queries for paginated and feed-style data
- Query invalidation after mutations to keep the cache coherent
What Redux is actually good for
None of this means Redux is bad. It means it was being used for the wrong problem. For genuinely complex client state — multi-step workflows, real-time collaborative features, deeply nested UI state that multiple parts of the app need to read and write — a dedicated state manager with predictable updates and good devtools is the right tool.
The teams I've seen get this right typically end up with a very thin Redux store (or Zustand, or Jotai) handling client-only concerns, and React Query handling everything that touches the network. The two libraries coexist without conflict, because they're solving different problems. The key shift is being deliberate about which tool handles which category of state, rather than defaulting everything into one place.
Conclusion
The simplification you get from separating server cache from client state is hard to appreciate until you've done it. Code that used to involve three reducers, two selectors, and a useEffect with a cleanup function now fits in a single hook call. The loading and error states come for free. The caching behavior is predictable and configurable.
More importantly, the remaining state in your store is actually state — things that are genuinely about what the user is doing right now. That clarity makes the application easier to reason about, easier to test, and easier to explain to a new team member. Sometimes the best architecture decision is recognizing which tool was never the right one for a particular job.