Redux was the right answer to the wrong question for a long time. The right question — where should state live? — has clearer answers now. Here are the patterns we use instead, and what we miss about the old way (not much).

What Redux was actually solving

Three things, mostly. The first was state that needed to be visible across far-apart parts of the component tree. The second was the need for time-travel debugging and a clean audit of state transitions. The third was a way to discipline a large team's state mutations through reducers.

Two of those problems still exist. The third has mostly evaporated.

Where state should live

The default — and it is a strong default — is to put state where it is used. A component that owns a piece of state is the simplest and most maintainable place for it to be. Lifted state is a tax, paid only when you need to.

When state does need to be shared, three layers handle it cleanly:

Server state lives in a server-state library.

Server state is not really client state — it is a cached view of the truth, which lives on the server. Treating it as client state is the original sin Redux was designed for. Libraries like TanStack Query (React Query) and SWR handle it natively: stale-while-revalidate, request deduplication, optimistic updates, cache invalidation. None of that has to be implemented in your store.

Form state lives in a form library.

Form state is hierarchical, validated, and tightly coupled to UI. React Hook Form handles it well; so does Formik. Either way, it has no business in a global store.

Genuine global state lives in context, plus a reducer.

What remains — theme, auth, feature flags, modal state — is usually small enough to live in a Context provider. If you want the discipline of reducers without the package, useReducer is in the standard library.

The order of operations: keep state local. Lift it when you have to. Reach for a library only when local-or-lifted stops working.

What we miss

Honestly, the dev tools. Redux DevTools gave you a clear audit trail of state changes that no replacement matches in full. We mitigate this with structured logging — a tiny utility that logs reducer transitions, mutation events, and route changes — but it is not the same as scrubbing through history.

We do not miss the boilerplate.

Three patterns to replace what Redux was good for

1. Co-locate, then lift.

Start with state in the component that uses it. Lift it only when a sibling needs to read it too. Once lifted, ask whether the new owner is the right one — sometimes a level higher is more honest.

2. Server cache, not store.

Use a server-state library for anything you fetch. Mutations go through the library's mutation API; the cache invalidates and refetches. The component does not own the truth; it observes it.

3. Provider per concern.

For genuinely global concerns, use a Context provider per concern (auth, theme, feature flags, etc.), each with its own useReducer. Do not stuff them into one omnibus store. The performance characteristics are better and the boundaries are clearer.

When Redux is still right

Two cases, roughly: you have an unusually large team that benefits from the strict mutation discipline reducers impose, or you have a hard requirement for time-travel debugging or replay. Most teams have neither.

Closing

Living without Redux is not a stylistic choice. It is a recognition that the right place for state is closer to where it is used than a single global store can ever be. The patterns that replace it are smaller, more local, and aged better.