Which state library should we use? I get the question almost weekly, and the answer that has stabilized for me over the last three years is shorter than the question deserves: Zustand by default, React Context for a small set of tree-wide values, Redux only when the team specifically needs what Redux gives them, which is rarely. The conversation that follows the question usually reveals that the team was about to install Redux for state that should have been a useState, or wrap the entire app in a Context provider for state that updates ten times a second and would rerender the world.
This article is the answer I now write up once and link instead of having the conversation again. The stance: client state libraries are a small toolkit, each tool fits a narrow band of problems, and the wrong tool produces either too much boilerplate (Redux for trivial state) or too many rerenders (Context for high-frequency state). The right one fades into the background and lets you write product code.
A scoping note before I start: this article is about client-side state. Server state (data from your API) belongs in a query library (TanStack Query, SWR, RTK Query, Apollo). Mixing the two is a separate mistake; do not put server data in Zustand or Redux just because you have a store. Server data has a cache, an inflight state, a stale state, and revalidation rules that query libraries handle and state libraries do not. Pick the right tool for that bucket separately.
The three options, in one paragraph each
Context is React's built-in mechanism for passing values down a component tree without prop-drilling. It is not really a state manager; it is a value broadcast channel. You can put state in it via useState paired with a Provider, and that works for small cases.
Zustand is a tiny external library (around a kilobyte gzipped). You create a store with create((set) => ({ ... })), you read from it via a hook (const count = useStore((s) => s.count)), and you write to it via the set function inside actions. There is no Provider, no reducer, no action types, no thunks. The hook subscribes the component to a slice of the store, so unrelated state changes do not trigger re-renders.
Redux is the venerable state manager from 2015, modernized as Redux Toolkit (RTK) since 2019. Reducers handle actions to produce next state, the store is a single tree, components subscribe via useSelector. RTK simplified a lot of the historical boilerplate (slices replace reducers + action creators in fewer lines, RTK Query handles server state). Redux's main draw in 2026 is the DevTools: time-travel debugging, action replay, full audit log of every state transition.
Context: when it is the right answer
Context is a value broadcast channel, not a state library. The right uses are tree-wide values that change rarely and are read by many components:
- The current theme (light/dark).
- The currently authenticated user's basic info.
- The current locale (for i18n).
- The current Mantine
themeor design-system root config.
The defining characteristic: the value is read in many places and changes infrequently. Theme switches once when the user toggles. Auth changes once at login and once at logout. Locale changes when the user changes the dropdown.
The shape:
The trap that catches teams: putting frequently-changing state into a Context. Every Context consumer re-renders every time the Context value changes, even consumers that only read a part of the value that did not actually change. A Context with a { user, cart, notifications, currentRoute } shape that updates when any of those four change re-renders every consumer on every change. Performance falls off a cliff in apps with many consumers.
The rule: Context for low-frequency tree-wide values. For higher-frequency state, use a real store with selector subscription, where each component only re-renders when its own slice changes. Zustand is exactly that.
Zustand: my default
Zustand is my default for any client state that is not a trivially local useState. The way I describe it to teammates: Zustand is a global object you can subscribe to with selectors, and the subscription is per-selector, not per-store.
The store definition:
Thirty lines including the type definition, and that store is fully usable. No Provider needed; the store is a module-level value. Components opt in by reading slices via the hook:
The selector is the key. useCartStore((s) => s.items.length) triggers a re-render only when the result of the selector changes. Adding an item changes items.length, so the count component re-renders. Updating an item's quantity (via a different action that mutates a single item) might not change items.length, in which case the count component does not re-render at all.
What I like about Zustand:
- No Provider. The store is a module-level singleton. Components import the hook directly. No "forgot to wrap in Provider" runtime errors.
- Per-selector subscriptions. Components only re-render when their slice changes. Performance is good by default; you do not need
React.memooruseMemoto fix re-render storms. - No reducers, no action types. Mutations are functions on the store that call
set(). The boilerplate is the inline action definition; there are no separate files for action creators. - TypeScript inference works. The store interface is the only thing you write; selectors and actions infer their types correctly.
- Middleware for the things you need. Persistence to localStorage, devtools integration, immer for nested updates, all available as composable middleware.
What I do not like:
- No time-travel devtools by default. Adding the
devtoolsmiddleware gets you a Redux-DevTools integration, but it is not the same depth as Redux's first-class devtools support. - No enforced single source of truth. You can have multiple Zustand stores, and that is sometimes a feature and sometimes a foot-gun. A team that is not careful ends up with three stores that each own a piece of the same domain.
- Easy to misuse for server state. Zustand stores are not query caches; putting server-fetched data in a Zustand store and writing your own refetch logic reinvents what TanStack Query does well.
For the median client-state need, none of those drawbacks bite. Zustand is what I reach for first.
Redux: where it still earns
Redux's main argument in 2026 is its devtools. The Redux DevTools Extension lets you see every action that has fired, the state before and after, time-travel back to any point, replay actions, export traces. For an app where you genuinely need to debug a reproducible bug six steps deep into a state machine, Redux's devtools are unmatched.
The second argument: a team that already has a large Redux codebase. Migrating away from Redux to Zustand is rarely worth it. The boilerplate is real but tolerable, and the team has internalized the patterns. "Redux is fine" is a defensible position when the codebase is already using it.
The third argument: complex state machines where the explicit reducer / action shape is itself helpful documentation. Some teams prefer the discipline of "every state transition is a named action with a reducer" because it forces the state graph to be visible in code. That is a stylistic preference more than a technical requirement, but it is not wrong.
A Redux Toolkit slice, for fairness:
RTK's createSlice is much shorter than vintage Redux. Immer is built in, so the reducer can mutate state directly. The action creators are auto-generated from the reducer keys. This is roughly the same volume of code as the Zustand version, with the addition of a Provider and a store-config file.
Where Redux still hurts: for trivial state ("is this dropdown open"), Redux is wildly overkill. A useState covers it in one line. Putting it in Redux means action types, a reducer, a selector, and a dispatch call to get back to where useState started.
The decision flow I run
When a team asks me what state library to use, the questions I ask in order:
- Is this state local to one component? If yes,
useState. Done. Do not put it in any library. - Is this state shared across a few sibling components? If yes, lift state to the common parent and pass via props. Still no library needed.
- Is this state tree-wide and infrequently changing? Theme, auth, locale. Use Context.
- Is this state shared across the app and changes more often than once a minute? Cart, modal stack, filter state, draft state, undo history. Use Zustand.
- Do you specifically need time-travel debugging or audit-log devtools? Redux.
- Do you already have a Redux codebase? Stay on Redux unless there is a real reason to migrate.
Most product teams sit at step 4 for the bulk of their state, and Zustand is the right answer for them. The teams who genuinely need step 5 know who they are: complex financial software, design tools with extensive undo histories, debuggers, IDEs.
A note on Jotai, Recoil, MobX, Valtio
A short note for completeness, since people ask. Jotai (atom-based, similar mental model to Recoil) and Valtio (proxy-based, mutate state directly) are both viable Zustand alternatives. They have nicer ergonomics for some patterns (Jotai's atoms are excellent for derived state). I have used Zustand more than either, and the differences are not large enough that I would push a team to switch. Recoil is effectively unmaintained as of 2024 and I would not start a new project on it. MobX is the OOP-flavored option; perfectly fine, less popular in the React ecosystem now than it was in 2018, but no technical reason to avoid it.
The metric that matters more than which specific library: per-selector subscription. Any modern store library that gives you per-selector subscriptions (Zustand, Jotai, Valtio, Redux with useSelector) avoids the Context re-render trap. Pick one of those and your app's state architecture is fine; the differences between them are mostly stylistic.
When I would pick Redux over Zustand on a new project
The one scenario where I genuinely reach for Redux on a brand-new project: the app is centrally about state transitions and the user expects undo / redo / replay as a feature. A drawing tool. A collaborative spreadsheet. A music DAW. In those, every user action is an event, the state graph is the substance of the app, and Redux's reducer-and-action shape is the natural model. Time-travel debugging is not just useful for developers, it is part of the product (Cmd+Z is implemented by replaying actions in reverse).
For a CRUD product, an admin panel, a SaaS dashboard, an e-commerce front-end, a content site, a marketing app, a mobile-web companion to a native app: Zustand. Every time. The Redux ceremony will not earn its keep, and the productivity loss compounds.
The summary I give the team that asked: install Zustand, write your stores as you need them, be ready to add Context for the truly tree-wide stuff, and reach for Redux only if your future self thanks you for it. Most future selves do not.
