If you’re still picking a state management library because of a Twitter argument from 2019, you’ve missed the point. In 2026, React state management isn’t a library contest. It’s a discipline. The ecosystem has calmed down. Redux vs. Context vs. MobX debates feel like ancient history now. What actually matters is granular control, keeping state close to where it’s used, and knowing exactly when to reach for a server cache instead of a client store. I still see teams dumping everything into a single global object or slapping a state machine on a dropdown toggle. This article is a hard reset—patterns I rely on to ship things that don’t fall apart a month later.

The Three-State Rule: Local, Page, and Global
Most React apps don’t need more than three buckets. The first mistake? Treating all state the same. The second? Prematurely dumping everything into a global store because it feels organized.
Local state stays inside a single component or a tiny subtree. useState or useReducer is the whole toolbox here. A toggle, a form input, a dropdown’s open/closed status—seriously, they don’t need a home in Zustand or a context provider. Keeping them local slashes re-renders and makes the component a self-contained unit you can test without a bunch of providers.
Page state is for a route or a feature screen. Think multi-step checkout or a dashboard filter panel. The pattern is a slice owned by a route-level provider. React Context plus useReducer works if you memoize the value and split context consumers carefully. But honestly, in 2026 a lot of us have moved to lightweight external stores like Zustand or Jotai for page-level stuff. They dodge the context-re-render tax completely. You spin up a store instance scoped to the page lifecycle and destroy it when the route unmounts. Clean and fast.
Global state is the stuff genuinely shared across unrelated parts of the app—authenticated user, theme prefs, feature flags. This is where one well-typed store earns its keep. Zustand is still the go-to for its tiny API and React 19 compatibility. Jotai’s atomic model is also solid if you like building state from small composable units. The trick is keeping the global store thin. If it starts hoarding UI state for a specific page, yank it out.

Server State vs. Client State: Stop Merging Them
The biggest anti-pattern I still stumble over in 2026 is treating server-fetched data as client state. You fetch a list of users, shove it into a Redux slice, write reducers to update, delete, and sort—congratulations, you’re now manually maintaining a cache. Please don’t.
Server state is anything born from an API where the source of truth lives on the server. React Query (TanStack Query) and SWR handle caching, background refetching, optimistic updates, and cache invalidation better than any hand-rolled solution I’ve ever seen. They also plug into React 19’s streaming and Suspense boundaries. The pattern: use React Query for all GET requests and cache interactions. A lightweight client store handles only UI state that isn’t chained to server data.
For mutations, the same libraries give you useMutation hooks that track loading, error, and success states. There’s zero reason to dispatch a Redux action that calls an API and then manually updates three slices. Let the server-state library invalidate caches and trigger re-fetches automatically. It’s faster and you write fewer bugs.
A practical rule: if the data has a URL endpoint, it’s server state. If it’s ephemeral and only exists in the browser tab—like a modal’s visibility or a temporary draft—it’s client state. No exceptions unless you enjoy pain.
Derived State and Selectors: Compute, Don’t Duplicate
Storing derived values in state is a bug factory. If fullName is always ${firstName} ${lastName}, don’t put it in the store. Derive it in a selector or right in the component with useMemo if the computation actually costs something.
In Zustand, selectors let you subscribe to a computed slice. The component only re-renders when the output of that selector changes. This kills the need for useEffect chains that sync state A to state B. Jotai’s derived atoms do the same thing with a different syntax. The mindset shift: treat state as a directed acyclic graph of dependencies, not a flat object you manually synchronize until your brain melts.
This also covers filtering and sorting. Got a list of items and an active filter? Derive the filtered list instead of storing it separately. React Query’s select option lets you transform server data before it reaches the component, so the cache stays normalized and the UI layer stays simple.
Immutable Updates and the Mutable Trap
React’s rendering model lives and dies by referential equality. Mutating state directly is still the number-one cause of stale UI and missed re-renders. In 2026, Immer is the standard fix—it lets you write straightforward mutable-style logic inside immutable update functions. It’s baked into Redux Toolkit and available as a standalone produce function for Zustand or useReducer.
The pattern: use Immer whenever an update touches nested objects or arrays. For flat state, the spread operator is fine. But the moment you’re updating users[3].profile.settings.theme, grab produce. It’s quicker to write and way easier to review than multi-level spreads that look like someone played Twister with the dot operator.
TypeScript makes this safer. Define your state shape with strict types, and Immer enforces immutability while letting you write draft.user.name = 'new name'. The combo of TypeScript and Immer catches accidental mutations at build time instead of runtime, which means you fix them before your coffee gets cold.

URL as State: The Forgotten Store
The URL is a state container that survives page reloads, supports browser navigation, and is shareable by default. In 2026, more teams treat URL parameters as the primary source of truth for search queries, pagination, and filter selections. React Router’s search params hooks or Next.js’s useSearchParams make this feel almost too easy.
The pattern: lift transient UI state that defines what the user is viewing into the URL. A search input’s value can stay in local state, but the applied search term belongs in the query string. Page numbers, sort order, tab selection—all of it goes into the URL. This eliminates the dance of syncing a Redux store with browser history and makes deep linking free.
When you combine this with React Query, you can derive the query key directly from URL parameters. Change the URL, React Query refetches automatically if the data isn’t already cached. You get a single flow: user action → URL update → derived query key → server-state fetch → UI render. No intermediate client store required. It’s elegant, and it frustrates me that more codebases don’t do it.
Performance Patterns That Actually Matter
State management performance in React comes down to one thing: stopping unnecessary re-renders. The tools give you the knobs, but you have to know which ones turn and which ones are just decoration.
- Split context providers vertically. Instead of one giant context with twenty values, break it into smaller contexts that each own a single responsibility. A component consuming only
CurrentUserContextwon’t re-render whenThemeContextupdates. It’s borderline therapeutic. - Use atomic state for high-frequency updates. Jotai’s atoms subscribe at the individual value level. If you’re updating a counter 60 times per second, an atomic model prevents the entire component tree from reconciling. Zustand’s selectors get you similar granularity.
- Memoize selectors with
useShallow. Zustand’suseShallowhook does a shallow comparison on the returned object, so a component that selects{ user, theme }won’t re-render if onlynotificationschanged. This is simpler and less error-prone than wrapping everything inuseCallbackanduseMemoby hand. - Lazy load state slices. For large apps, code-split the state initialization. Load the admin dashboard state only when that route mounts. Zustand’s
createfunction works at module scope, but you can dynamically create stores insideuseEffectand clean them up on unmount.
The overarching principle: measure before you optimize. React DevTools Profiler shows exactly which components re-render and why. Fix the hotspots, not the entire app. Your future self will thank you when you’re not untangling a web of React.memo wrappers.
Putting It Together: A 2026 State Architecture
Here’s a concrete architecture that fits the patterns above. It’s not hypothetical—this is what I ship in production React apps today.
- TanStack Query for all server state. Configure a query client with sensible defaults: stale time of 30 seconds, retry 1, garbage collection of 10 minutes. Use
queryOptionsobjects to share query definitions across components. It keeps things tidy and predictable. - Zustand for global client state. Keep it under 10 keys. Auth token, user preferences, feature flags. Use
persistmiddleware for anything that needs to survive a page refresh. - React Context + useReducer for page-level state that multiple components on a route share. Wrap the route segment, not the entire app. It’s a focused scope, not a blanket.
- URL search params for any state that defines the view: filters, pagination, sort. Sync them with React Query’s query keys using a custom hook.
- Local state for everything else. Don’t overthink it. A
useStatein the component is the most performant option you have.
This stack strips out the middleware layers, the boilerplate, and the mental overhead that made earlier React state management feel like a part-time job. You write less code, and the data flow is explicit enough that a new developer can trace it in a single reading. That’s the goal—clarity that doesn’t vanish when the sprint pressure hits.
Frequently Asked Questions
Is Redux still relevant in 2026?
Redux Toolkit is still maintained and works fine, but it’s not the default pick for new projects anymore. Its strength is in large, established codebases with complex reducer logic and middleware needs. For most new apps, Zustand or Jotai paired with React Query covers the same ground with less ceremony. If you’re on an existing Redux codebase, migrating away isn’t a high priority—Redux Toolkit’s patterns are solid enough. But for greenfield work, start lighter and don’t look back.
How do I handle form state without a library?
React Hook Form is still the standard, but for simple forms, React’s built-in useActionState (stable in React 19) handles server actions and validation without extra dependencies. The pattern: use useActionState for forms that submit to a server action, and fall back to React Hook Form for complex client-side validation with dynamic fields. Keep form state local to the form component unless multiple pages share the same form instance, which almost never happens in practice.
When should I use useReducer instead of useState?
Reach for useReducer when the next state depends on the previous state in a non-trivial way, or when one action updates multiple state values at once. A toggle is useState. A multi-step wizard where “next” increments the step, saves the current step’s data, and resets validation errors is useReducer. It also shines when you need to pass a dispatch function down to deeply nested components instead of a cascade of setter callbacks.
Can I mix Zustand and React Query in the same project?
Absolutely. They solve different problems. React Query manages server cache; Zustand manages client-side state that isn’t tied to a specific API endpoint. A common pattern is to use Zustand for the authenticated user object (fetched once and needed everywhere) and React Query for all other API data. They don’t step on each other’s toes, and you can even reference a Zustand store inside a React Query queryFn if you need an auth token for a request. It’s a clean division of labor.