JournalReactMar 18, 2025

Why I Stopped Using Global State — And What Replaced It.

After years of Redux boilerplate and Context re-renders, I moved to co-located server state with React Query. The codebase lost weight. The team gained velocity.

10 Min Read
Why I Stopped Using Global State — And What Replaced It.

I remember the exact moment I stopped believing in global state. It was a Tuesday afternoon, three hours into debugging a re-render cascade that started in a notification badge and somehow caused a shipping form to lose its draft data. The culprit was a Context provider four levels up that triggered a re-render on every WebSocket message. The fix took twenty minutes. Finding it took the entire day.

For years, Redux had been the answer to every state management question. Need to share data between components? Redux. Need to cache an API response? Redux. Need to track a modal's open state? Somehow, also Redux. The store grew. The boilerplate multiplied. And the team spent more time writing action creators and reducers than building features.

The shift started at mPower, where I was leading frontend on platforms built in collaboration with WHO and USAID. These were applications where data integrity was not a nice-to-have — it was a mandate. API responses needed to be fresh, cached intelligently, and invalidated precisely. Redux could do this, but only with middleware bolted onto middleware, and a state shape that looked more like a database schema than a UI concern.

React Query changed the framing entirely. Instead of asking "where does this data live in the store?", the question became "when was this data last fetched, and is it still valid?" Server state — the data that originates from an API — was no longer something I managed. It was something I subscribed to.

The migration was not a rewrite. It was a gradual extraction. One feature at a time, we replaced Redux slices with `useQuery` hooks. Each migration followed the same pattern: remove the reducer, remove the action, remove the selector, add a query hook with a stale time and a cache key. The Redux store shrank. The component code became shorter. The data flow became obvious.

The results were measurable. API-related bugs dropped by roughly forty percent in the first quarter after adoption. Not because React Query is magic, but because it eliminated an entire category of mistakes — stale data, duplicate fetches, race conditions between sequential requests. These bugs had been a constant background noise. When they stopped, the silence was striking.

Local state went back to being local. A modal's visibility is `useState`. A form's draft data is `useState`. A stepper's current index is `useState`. None of these need to be global. None of them need to survive a page navigation. Putting them in a global store was never a technical requirement — it was a habit formed by tools that made global state too easy and local state feel inadequate.

The team adapted faster than I expected. Junior developers, in particular, found the mental model simpler. "This hook fetches the data. This key identifies the cache. This function invalidates it." Three concepts instead of thirty. The onboarding cost of the state management layer dropped from days to hours.

I am not anti-Redux. There are legitimate use cases for client-side global state — complex multi-step workflows, optimistic updates with rollback, offline-first architectures. But these are specific, advanced scenarios. They are not the default. And treating them as the default is how codebases end up with a store that knows everything about everything, and components that cannot render without consulting it.

The principle I carry forward is this: the best state management strategy is the one with the smallest surface area that still solves the problem. Start with local state. Reach for server state management when your data comes from an API. Reach for global state only when you have proven that nothing else works. In five years of applying this principle, I have reached for global state exactly twice.