Web / React
React State Management in 2026: Context, Zustand, Jotai, Redux, and Beyond
React state management in 2026 is less about picking a library and more about classifying your state correctly: server state (remote, async, cacheable) belongs in a query library like TanStack Query or a server component, local UI state belongs in useState, and only genuinely global client state — a theme, a cart, an auth session — needs a store like Zustand, Jotai, or Redux Toolkit. Most “state management problem” horror stories are really server-state problems handled with the wrong tool.
The Crucial Split: Server State vs Client State
Before comparing libraries, split state into three buckets — this taxonomy is the single most useful mental model in modern React:
- Server state: Data that lives on a server and is fetched over the network — user profiles, product lists, search results. It is remote, asynchronous, shared across users, and can become stale at any moment. It needs caching, refetching, and invalidation — not a global store.
- Local UI state: State owned by a single component or a small subtree — an open/closed modal, a hover flag, a form field. This is what
useStateanduseReducerare for, and most state in most apps lives here. - Global client state: State that truly is shared across distant parts of the component tree and lives on the client — theme, locale, shopping cart, auth token, feature flags, a complex wizard step. Only this bucket needs a store.
The classic React mistake was treating server state as global client state: fetching data into a Redux store, hand-writing loading and error flags, and debugging cache-invalidation bugs forever. The 2020s corrected this — first with React Query (now TanStack Query), later with Server Components — and that correction is why the state-management landscape looks so different from 2019.
Server State: TanStack Query and Server Components
TanStack Query became the default client-side answer to server state because it codifies what every hand-rolled fetch layer reinvents: a keyed cache, background refetching, stale-while- revalidate, pagination and infinite queries, mutations with optimistic updates, and retry logic. Instead of storinguser: { name } in a global store, you query it with a key and let the library manage freshness.
React Server Components pushed the boundary further. When data fetching happens on the server, the client never holds the server state at all — the rendered result streams down as UI. With frameworks supporting streaming SSR, use() in React 19, and framework-level data APIs, a large class of apps needs no client-side data store whatsoever. TanStack Query remains essential for interactive, client-fetched, or offline-tolerant apps — the two approaches coexist: server components for initial and cacheable reads, the query library for everything interactive.
When useState and useReducer Are Enough
For local UI state, React's built-ins remain the correct answer, and reaching for a library earlier than needed is the most common architectural error in React codebases. A modal open flag, a multi-step form wizard, a dropdown selection, a drag-and-drop board where columns live in one component — all of these are plain state.
A useful rule: if the state is created in a component, owned by that component, and only needs to be passed to its direct children, do not put it in a store. Lifting state to a context or a global store because “we might need it later” creates re-render fan-out and an API surface you maintain for no benefit. Start local; extract only when a real cross-tree consumer appears.
Context: Useful, but Misunderstood
React Context is designed for low-frequency, dependency- injection-style state: the current theme, the current locale, an auth user object, a router. It passes values down the tree without prop-drilling, and it is perfect for that job.
Its two well-documented limitations define where it stops being appropriate:
- Re-render fan-out: When a context value changes, every consumer re-renders. There is no selector mechanism — a component that only reads
theme.darkstill re-renders whenuser.namechanges, if they share a context. - Not for high-frequency updates: Context is optimized for values that change rarely. Putting cursor positions, drag coordinates, or animation progress through context causes a full consumer-subtree re-render per change — the classic cause of janky React UIs.
The practical pattern: use Context for values that change a few times per session; use a store with selectors for anything that updates frequently or lives outside the React tree.
Zustand: The Minimal Store
Zustand (from the Poimandres collective) became the most popular “just a store” library for React. A hook based API, no providers required, a tiny footprint (roughly 1 KB gzipped for the core), and selector-based subscriptions that only re-render components when the selected slice changes. It works inside and outside React components, ships middleware for persistence, devtools, andimmer, and integrates withuseSyncExternalStoreunder the hood.
import { create } from "zustand";
import { persist } from "zustand/middleware";
interface CartState {
items: { id: string; qty: number }[];
addItem: (id: string) => void;
removeItem: (id: string) => void;
}
export const useCartStore = create<CartState>()(
persist(
(set) => ({
items: [],
addItem: (id) =>
set((s) => ({
items: [...s.items, { id, qty: 1 }],
})),
removeItem: (id) =>
set((s) => ({
items: s.items.filter((i) => i.id !== id),
})),
}),
{ name: "cart" }
)
);
// Any component can subscribe; only components whose
// selected slice changes re-render.
function CartCount() {
const count = useCartStore((s) => s.items.length);
return <span>{count} items</span>;
}Zustand is the pragmatic default in 2026 for global client state: a cart, a sidebar, a draft editor, WebSocket-connected UI state. Its simplicity is the point — there is almost no API surface to learn, and no provider tree to set up.
Jotai: The Atomic Model
Jotai takes the opposite mental model from a single store: state is a graph of atoms — primitive values, derived computations, and async atoms — and components subscribe to exactly the atoms they read. Bottom-up declaration, fine- grained updates, and colocated state trees suit apps with many independent pieces of shared state (dashboards, editors, music apps) better than one flat store.
import { atom, useAtom } from "jotai";
import { atomWithStorage } from "jotai/utils";
// Primitive atoms: independent, minimal subscriptions
const queryAtom = atom("");
const pageAtom = atom(1);
// Derived (computed) atom: recalculates only when inputs change
const resultsAtom = atom(async (get) => {
const q = get(queryAtom);
const page = get(pageAtom);
const res = await fetch(
`/api/search?q=${q}&page=${page}`
);
return res.json();
});
// Persisted atom
const themeAtom = atomWithStorage<"light" | "dark">(
"theme",
"light"
);
function SearchBox() {
const [query, setQuery] = useAtom(queryAtom);
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
);
}Jotai shines when derived state matters — computed atoms recompute only when their dependencies change, and you avoid the selector boilerplate a large Zustand store accumulates. The trade-off is a more conceptual API (atom families, scopes, providers for isolation) that some teams find harder to onboard onto than a plain store.
Redux Toolkit: The Boilerplate Is Gone
The Redux of 2018 — action creators, hand-written reducers, connect(), thunks wired by hand — is dead. Redux Toolkit (RTK) collapsed it to configureStore,createSlice, andcreateAsyncThunk — a typical slice is a fraction of its former size, Immer is built in, and Redux Query / RTK Query handles server-state caching inside the same store if you want it.
What keeps Redux relevant in 2026 is not nostalgia but its ecosystem: the best-in-class devtools time-travel debugger, a decade of middleware (sagas, observables, offline persistence), RTK Query as a full data-fetching layer, and the largest pool of experienced React developers. For large teams where consistency and debuggability outrank bundle size, RTK is still a defensible choice — and the boilerplate argument against it stopped applying when RTK shipped in 2019.
Signals in the React Ecosystem
Signals — fine-grained reactive values with automatic dependency tracking, popularized by SolidJS, Preact, Angular, and Vue — spent the early 2020s arriving in React land through third-party libraries (Legend-State, Valtio, Preact signals via compat, and several TC39-stage experiments). The important 2025–2026 development: a TC39 Signals proposalreached Stage 1, aiming to standardize a framework-agnostic reactive primitive in the language itself, backed by engineers from multiple framework teams.
In React, signal libraries can outperform store subscriptions for high-frequency UI updates because many bypass React's render cycle for the fine-grained parts. But they trade away React's mental model — components no longer re-render as the unit of update — and add an API foreign to the rest of the ecosystem. For most teams in 2026, signals remain an interesting frontier rather than a default choice; if you adopt one, do it at a boundary (a canvas-heavy or drag-heavy widget), not app-wide.
Comparison: Zustand vs Jotai vs Redux Toolkit
| Dimension | Zustand | Jotai | Redux Toolkit |
|---|---|---|---|
| Model | Single store + selectors | Atom graph (bottom-up) | Single store, slices |
| Core size (gzip) | ~1 KB | ~3 KB | ~11 KB + RTK Query |
| Learning curve | Very low | Low–medium (atoms, scopes) | Medium (actions, slices, middleware) |
| Devtools | Middleware bridge | Jotai devtools | Best-in-class time travel |
| Server state | Pair with TanStack Query | Async atoms possible | RTK Query built in |
| Provider needed | No | Optional (for isolation) | Yes |
| Best for | Simple global UI state | Many derived/independent values | Large teams, strict conventions |
Sizes are approximate core-only figures; all three are far smaller than pre-2020 state libraries, and none is a performance liability at real-world scale. The differences that matter are API model, team familiarity, and tooling — not kilobytes.
A Decision Tree for 2026
Run through this in order; stop at the first yes:
- Is it data fetched from a server? Use a server component or TanStack Query. Never a client store.
- Is it owned by one component or a small subtree (modal, form field, hover)? Use useState or useReducer.
- Is it rare-change dependency injection (theme, locale, authenticated user)? Use Context — with separate contexts per concern so values can change independently.
- Is it genuinely global client state with moderate update frequency (cart, sidebar, drafts)? Zustand is the default; Redux Toolkit if your team already standardizes on it.
- Is it a graph of many independent or derived values, or state shared across unrelated widgets? Jotai (or similar atom libraries) fits better than a flat store.
- Is it high-frequency interactive state (drag, canvas, animation) where React re-renders themselves cause jank? Consider a signals library at that boundary — not app-wide.
Migration Notes
Teams migrating older React apps usually face one of two situations:
- Redux app with server data in the store: Extract fetch logic into TanStack Query (or server components) first — that is where the largest win is. Then shrink the remaining store to true client state and decide whether to keep RTK or move to Zustand. RTK Query is a legitimate middle path if you want one library.
- Context app with performance issues: Split the context by change frequency, memoize values, and move anything high-frequency into a selector-based store. Most Context performance crises resolve with a two-way split into a rarely-changing context and a Zustand slice.
- Any app adding TypeScript: Whatever store you pick, type the state interface once and derive action types from it — RTK and Zustand both make this nearly free, and it prevents the slow drift of untyped store shapes.
Conclusion
React state management stopped being a library war when the ecosystem absorbed its two big lessons: server state is not client state, and most state is local. TanStack Query and Server Components own the remote half; useState owns the local half; and a small selector-based store — Zustand for most teams, Jotai for atom-shaped problems, Redux Toolkit for teams that value its tooling and conventions — covers the genuinely global remainder.
Pick by classifying state, not by popularity. The stack that gets the classification right will feel boring — which, in state management, is the highest compliment.
Key Takeaways
- Classify state before choosing a tool: server state (TanStack Query or server components), local UI state (useState/useReducer), global client state (a store).
- Context is for low-frequency dependency injection — it re-renders all consumers and is wrong for high-frequency updates.
- Zustand (~1 KB, no provider) is the pragmatic default for global client state; Jotai's atom model fits many derived or independent values; Redux Toolkit removed the boilerplate and keeps best-in-class devtools.
- Signals arrived in React via third-party libraries, and a TC39 Stage 1 proposal aims to standardize the primitive — but for most teams they remain a boundary optimization, not an app-wide choice.
- Follow the decision order — server? → query library. local? → useState. rare-change shared? → Context. global? → store. — and most state-management complexity disappears.
FAQ
Do I need Redux for a new React app in 2026?
Almost certainly not by default. For most apps, useState covers local state, TanStack Query or server components cover server state, and a small store like Zustand covers the rest. Redux Toolkit remains a strong choice for large teams that want strict conventions and best-in-class devtools — but it is no longer the default answer it was in 2018.
What replaced Redux for server data?
TanStack Query (formerly React Query) became the standard client-side answer for server state, providing caching, background refetching, and optimistic updates out of the box. React Server Components moved even more of it server-side, where the client never holds the data at all.
Is Zustand better than Jotai?
Neither is strictly better — they model state differently. Zustand is a single store with selector-based subscriptions and a tiny API, ideal for typical global UI state like carts and sidebars. Jotai is an atom graph with fine-grained derived values, better when you have many independent or computed pieces of state. Most teams reach for Zustand first and consider Jotai when derived-state complexity grows.
Why does React Context cause performance problems?
When a context value changes, every component consuming that context re-renders — there is no selector to limit the update. Context is optimized for values that change rarely (theme, locale, user). High-frequency values like pointer coordinates or animation progress should live in a store with selectors, a ref, or a signals library instead.
Are signals coming to React?
A TC39 proposal to standardize signals in JavaScript reached Stage 1, backed by engineers from several framework teams, which would make a signals primitive available to any framework including React. In the meantime, React apps can use third-party signal libraries (Valtio, Legend-State, Preact signals), though most teams treat them as targeted optimizations rather than a replacement for React's render model.
References
- TanStack. "TanStack Query Documentation." tanstack.com/query
- React. "You Might Not Need an Effect" and "State: A Component's Local Memory." react.dev. react.dev/learn
- Zustand. "Zustand documentation." GitHub. github.com/pmndrs/zustand
- Jotai. "Jotai documentation." jotai.org. jotai.org
- Redux. "Redux Toolkit documentation." redux.js.org. redux-toolkit.js.org
- TC39. "Signals Proposal." GitHub. github.com/tc39/proposal-signals