Where We Are with State in 2026
React state management finally grew up. The old Redux boilerplate and those sprawling useEffect chains are mostly dead in serious codebases. What’s taken their place is a fairly quiet, practical consensus around a few patterns that don’t buckle when your app gets big. Nobody’s arguing about which library is best anymore. The real question is where you put the state and why you put it there. In 2026, solid React teams treat state like a design constraint, not a shopping trip for tools.
I’m Suki Watanabe. I’ve spent the last few years pulling apart state logic in production systems—fintech dashboards, real-time collaboration canvases, content-heavy consumer apps. The patterns here are the ones I reach for first. No fluff. No ceremony. Just what ships and stays shipped.

Pattern 1: Server State Stays on the Server
Most teams can recite the difference between server state and client state by now. Actually sticking the landing? That still slips. In 2026, the pattern that keeps things sane is treat the server as the source of truth, and treat the cache as a replica. TanStack Query (what we used to call React Query) and SWR both handle this well, but the pattern is bigger than any single library.
The rule in practice: if the data came from an API, don’t copy it into useState or a global store. Let the fetching library own the lifecycle—stale-time policies, background refetching, optimistic mutations. For a dashboard pulling portfolio data, I stick to one useQuery call per entity, pull the UI state right off the cache, and let the library revalidate on window focus or network reconnect. The component tree stays skinny, and I dodge the “dual source of truth” bugs that hand-rolled stores breed.
Here’s the shape it takes, concretely:
// Instead of useState + useEffect + global store:
function Portfolio({ userId }) {
const { data, isLoading, error } = useQuery({
queryKey: ['portfolio', userId],
queryFn: () => fetchPortfolio(userId),
staleTime: 5 * 60 * 1000,
});
if (isLoading) return <Skeleton />;
if (error) return <ErrorBanner />;
return <PortfolioChart data={data} />;
}
The decision that really matters here is staleTime. For fast-moving data like live pricing, I drop it to zero but keep the cache as a fallback. For user profiles, I set it to Infinity and manually invalidate on profile updates. The pattern scales because the cache becomes the single layer between the component and the network—no duplication, no drift.
Pattern 2: Colocate Client State, Then Lift It Only When You Must
Client state—form inputs, modal visibility, the selected tab—has the opposite rule: keep it as close to the consumer as you can. By 2026, the React world has mostly quit dumping UI state into global stores because, well, that’s what everyone did five years ago. The payoff is components that are easier to test and a lot easier to delete.
Start with useState inside the leaf component. If a sibling needs access, lift the state to the nearest common parent. Only grab context or a tiny Zustand slice when the consumer tree is deep or spread all over the place. I’ve walked into too many codebases where a Redux store carries an `isModalOpen` boolean that exactly one component and its direct child care about. That’s a tax you pay on every render and every refactor.

For shared UI state that actually needs to cross routes or worm through deeply nested trees, I use React Context with a small reducer, or a single Zustand store split into slices. The hard constraint: the store holds only serializable, UI-specific values. Business entities stay in the server cache. A real example from a multi-step onboarding flow: a Zustand slice holds the current step index, a draft object for form fields, and an `isSubmitting` flag. Everything else—validated data, available options—comes from TanStack Query. The boundary is clean, and the store is under 40 lines.
Pattern 3: Derived State Shouldn’t Be Stored
One of the sharpest cuts you can make in a React codebase is stripping out stored derived values. If you can compute a piece of data from existing state, compute it at render time—with useMemo when the computation is heavy—and never write it to state. In 2026, this is table stakes, but I still audit codebases where someone stored `fullName` separately from `firstName` and `lastName`.
The pattern I enforce: every piece of state in a component or store must be a source, not a result. For a shopping cart, the stored state is an array of item IDs and quantities. Total price, item count, discount eligibility—those are derived in a useMemo or, better, in a pure function outside the component that takes the cart array as input. This wipes out an entire class of synchronization bugs and makes the code easier to reason about immediately.
When the derivation gets expensive, I lean on useMemo with a clear dependency array. But I also trust the React compiler work that’s landed by 2026: automatic memoization is real in many setups, and I don’t scatter useMemo around defensively anymore. The pattern is to write clean, pure derivations and let the runtime do its job.
Pattern 4: URL State Is a First-Class Store
In 2026, the URL bar is the most underrated state container in React. For any state that should survive a refresh, be shareable, or drive server data fetching—put it in the URL. Search filters, pagination offsets, selected item IDs: these belong in query parameters or path segments, not in a Redux store or a context that evaporates on navigation.
The tooling here is solid. React Router v7 (or whichever routing library you’re on) makes it simple to read and write URL search params with hooks like `useSearchParams`. On a recent product catalog rebuild, I moved all filter state—category, price range, sort order—into the URL. The component tree reads the params on mount, plugs them into the server state query key, and updates the URL on user interaction via `setSearchParams`. The result: deep-linkable pages, zero client-side state for filters, and a back button that actually works.

The pattern pairs cleanly with server state management. The query key for TanStack Query becomes `[‘products’, searchParams.toString()]`, and the library handles data fetching off URL changes. When the user hits back, the URL changes, the query refires (or serves from cache), and the UI updates—no synchronization code needed.
Pattern 5: Immutable Updates with a Light Touch
Immutability isn’t a library choice in 2026; it’s a language habit. With the spread operator, optional chaining, and tools like Immer around, the pattern is: never mutate state in place, but don’t over-engineer it. In most production code I write, I use plain JavaScript spread for shallow updates and reach for Immer only when the state shape is deeply nested and the update logic would turn into a mess with spread alone.
For a complex form with nested sections, Immer’s `produce` function lets me write what looks like mutation while keeping the state tree intact. But I keep its use localized—a reducer inside a single component or a small Zustand slice. The wider application never needs to know Immer exists. That keeps the bundle impact low and the mental model simple.
The real pattern here is discipline: if you see a direct assignment like `state.items[3].name = ‘new’`, fix it right now. The cost of that bug in concurrent rendering and strict mode is too high to ignore.
Choosing Your Tools: A Quick 2026 Map
The libraries have settled into clear roles. Here’s the lineup I recommend based on what’s held up in production:
- Server state: TanStack Query v6. Mature, well-documented, handles caching, pagination, and optimistic updates out of the box.
- Client state (small to medium): React Context + useReducer. Zero dependencies, great for theme, auth status, or a small wizard flow. Keep the value stable with useMemo to avoid unnecessary renders.
- Client state (larger or cross-route): Zustand v5. Minimal API, no providers, supports slices and middleware like `persist` for localStorage. My default for anything that outgrows Context.
- Form state: React Hook Form with Zod validation. Handles complex fields, arrays, and validation schemas without re-rendering the whole form on every keystroke.
- URL state: React Router’s `useSearchParams` and `useParams`. No extra library needed.
Notice what’s missing: Redux, MobX, Recoil. They all still work, but the combination above covers 95% of use cases with less ceremony. If you’re on a legacy Redux codebase, the migration path is to carve out server state into TanStack Query first, then move UI slices to Zustand or Context piece by piece.
FAQ
When should I use a global store instead of React Context?
Use a global store (like Zustand) when the state needs to be accessed by many unrelated components across different parts of the tree, and when performance matters—Context can cause widespread re-renders if the value changes frequently. If the state is consumed by a single subtree and changes rarely (e.g., theme), Context is fine.
Is it okay to mix server state and client state in the same store?
No. This is a common source of bugs. Server state has a lifecycle (loading, error, stale) that client state doesn’t. Keep them separate: server data stays in TanStack Query or SWR, client UI state stays in a small store or local useState. If you need to combine them, do it in a hook that reads from both sources and derives the final shape.
How many state management libraries should a mid-size app use?
Two, maybe three: one for server state (TanStack Query), one for client state (Zustand or Context), and possibly a form library (React Hook Form). Adding more than that usually means you’re solving organizational problems with tools, or you’ve inherited a legacy stack you’re actively shrinking.
What’s the best way to handle optimistic updates in 2026?
TanStack Query’s `onMutate` callback is still the cleanest pattern. You snapshot the current cache, apply the expected change immediately, and roll back on error. Keep the optimistic update function pure and close to the mutation definition so the logic is easy to audit. Skip optimistic updates for operations that fail often or have complex side effects—the flicker isn’t worth it.
State management in React has finally become boring, and that’s a good thing. The patterns above aren’t flashy, but they’ve kept my teams moving fast and shipping with confidence. Start with server state, colocate the rest, compute what you can, and let the URL do the heavy lifting. The rest is details.