Why I Stopped Putting Server State in Redux
After years of writing Redux reducers that were just caching with extra steps, I switched to a three-layer state architecture that actually makes sense.
The Realization
I spent years writing Redux reducers that, in hindsight, were functioning as little more than a caching layer with extra ceremony: fetch data from an API, dispatch an action to store it, read it back out of the store in a component. The boilerplate — action types, action creators, reducers, selectors, async middleware — was substantial relative to the actual value delivered. A typical "fetch orders and display them" feature required a slice file with 80+ lines of code to do something that should take 10. The boilerplate was justified as the price of explicit, traceable state management, and there's merit to that argument, but the cost was real and it was paid on every feature.
A dedicated data-fetching library like TanStack Query accomplishes the same end result in 10 lines, and the reduction isn't just about typing less — it's about reducing the surface area for bugs. Every line of boilerplate is a line that can have a typo, drift out of sync, or require reviewer attention. Eliminating boilerplate eliminates an entire category of potential bugs. Redux still earns its place for genuinely complex client-side state — multi-step forms, real-time collaboration, undo/redo — but for server state, a dedicated library does the job better, with less code, and better performance out of the box.
The Three-Layer Architecture
The stack I settle into now has three distinct layers: TanStack Query owns server state, Redux Toolkit owns genuinely global client-originated state (auth, theme, layout preferences), and URL search params own filter and sort state that should survive a refresh or be shareable. Keeping those three layers cleanly separated removes most of the "where should this state live?" confusion that used to come up in code review. When the categories are clean, the answer is usually obvious: if it's from the server, TanStack Query; if it's global UI state, Redux; if it's something the user would want to bookmark, URL params.
The URL search params layer is the one most teams forget, and it's the one that pays the largest dividends in user experience. When filter and sort state live in component state, refreshing the page loses them, and they can't be shared as links. When they live in URL params, they survive refresh, they can be shared, they work with the back button, and they make the application's state observable just by looking at the address bar. A user reporting "the dashboard is showing the wrong data" can share the URL and you immediately see their filter state, instead of walking them through a debugging conversation about which filters they had applied.
