Comparison
Does a new React app still need Redux in 2026?
The verdict
For a new React application, start with Zustand. It is the 2026 default: about 3 KB against roughly 15 KB for Redux Toolkit with react-redux, no provider, no reducers, no action constants, and a store you can read and write from outside React. For a large existing codebase already on Redux Toolkit, stay there - the enforced patterns and time-travel debugging genuinely pay for themselves past about ten developers, and a rewrite buys you nothing a user will ever notice. The more useful answer is that for most teams neither one is the real fix. The state you are fighting is cached server data, and that belongs in TanStack Query or RTK Query, not in a global store at all.
The download numbers tell the story bluntly. Zustand now sits around 14 million weekly npm downloads against roughly 9.8 million for Redux Toolkit, and those figures were the other way around two years ago. Redux is not dying - it roughly doubled in the same window - but it stopped being the reflex answer to "how do I share state in React".
Zustand vs Redux Toolkit on the dimensions that decide it
Redux Toolkit is the only fair comparison point. Nobody has written hand-rolled Redux with action constants and switch statements in years, and comparing against that strawman is how most articles on this subject go wrong.
| Dimension | Zustand | Redux Toolkit |
|---|---|---|
| Bundle cost | Roughly 3 KB minified and gzipped for the core. | Roughly 15 KB with react-redux, before RTK Query. Immer and the middleware stack are included. |
| Setup | One create() call. No provider, no context, no wrapping the app. | configureStore, a Provider at the root, and a slice per domain. More ceremony, but it is prescribed rather than invented. |
| Writing state | Call set() from anywhere - a component, an event handler, a websocket callback, a test. | Dispatch an action to a reducer. Indirect on purpose: every change has a name and a place. |
| Use outside React | First class. store.getState() and store.setState() work in plain modules, which makes non-React integration trivial. | Also possible, since the store is a plain object, but the ergonomics assume you are inside the React tree. |
| Re-render control | Selector-based subscriptions by default. A component re-renders only for the slice it selected. | useSelector with reference equality, plus createSelector for memoized derived state. Equivalent, with more machinery. |
| Devtools | Connects to Redux DevTools through the devtools middleware. Action names are whatever you label them. | The strongest remaining argument for Redux. Every action named, an action log, time travel, and state diffing that just works. |
| Middleware | persist, immer, devtools, subscribeWithSelector, combine. Covers the common cases and stops there. | A decade of ecosystem: listener middleware, sagas, observables, offline queues, undo stacks. |
| Server state | Not its job. You pair it with TanStack Query and keep the store for client state only. | RTK Query is included and good: caching, invalidation, polling, and generated hooks in the same package. |
| Team scale | Freedom becomes inconsistency without conventions. Three developers will write three different store shapes. | Prescribed structure. On a large team that is a feature, because a new hire can read any slice and know where things go. |
| Migration cost | Adopting it is incremental - add one store, leave everything else alone. Both libraries can run side by side. | Leaving it is a per-slice rewrite. Doable incrementally, but every selector, thunk, and test moves with the slice. |
When each library is the right call
Choose Zustand when
- You are starting a new app and want the smallest thing that solves the problem you actually have today.
- Bundle size is a metric someone tracks. Twelve kilobytes is not nothing on a mobile connection, and it is pure overhead if you never use the extra machinery.
- You need to touch state from outside React: a websocket handler, a service worker message, an imperative animation callback, a test setup file.
- The team is small enough that conventions can live in review rather than in a framework.
- Most of your data is already in TanStack Query, and what remains is a theme, a sidebar toggle, a wizard step, and a draft form.
- You want persistence with one line. The persist middleware writes to localStorage or AsyncStorage with a version and a migration hook.
Choose Redux Toolkit when
- The codebase is already on Redux. Migrating a working store is a rewrite with no user-visible benefit, and there are better places to spend that quarter.
- Ten or more developers touch the state layer. Enforced structure is worth more than saved keystrokes once "where does this go" is asked daily.
- Debugging is the bottleneck. A named action log and time-travel replay of a reproduction is still the best debugging experience in React, and nothing else is close.
- You need complex asynchronous orchestration - cancellation, retries, sequencing, long-lived workflows - where the listener middleware or sagas earn their weight.
- You want server-state caching and client state in one dependency. RTK Query is genuinely good and removes a decision.
- Your domain is genuinely event-shaped: collaborative editing, undo stacks, audit trails. Modeling those as a stream of named actions is the correct design, not overhead.
When neither one is the real answer
Before picking either, audit what is actually in your global store. On most teams the majority of it is server data: lists fetched from an API, a current user, a set of records being edited, a cache of lookups. That is not application state, it is a copy of someone else's state with a staleness problem, and putting it in Redux or Zustand means hand-writing loading flags, error flags, refetch logic, cache invalidation, request deduplication, and retry behavior that a query library already ships.
Move that to TanStack Query, or to RTK Query if you are already on Redux, and watch what is left. Usually it is a theme preference, a sidebar toggle, a multi-step form draft, a set of active filters, and the auth session. That is a Zustand store of forty lines, and it makes the Zustand-versus-Redux argument mostly moot - which is the real reason Redux usage flattened. It did not lose to Zustand. It lost to the realization that most of what it was holding never belonged in a global store.
Two more places state does not belong in a library at all. Filters, tabs, pagination, and search terms belong in the URL, where they are shareable, bookmarkable, and survive a refresh for free. And plain useState lifted to the nearest common parent is still the right answer for anything used by two sibling components; reaching for a global store to avoid passing one prop is how stores become unmaintainable.
React 19 also absorbed some of this. useActionState and useOptimistic cover form submission state and optimistic updates natively, and a Server Component that reads data on the server never needs a client store for it at all. In a Next.js App Router project, a meaningful fraction of what a 2020 codebase kept in Redux does not exist as client state anymore.
Official documentation is at zustand.docs.pmnd.rs and redux-toolkit.js.org. Redux's own documentation has been unusually honest about this for years, explicitly telling readers that not every app needs Redux, which is worth reading before you adopt or abandon it.
Where this fits in a full stack
Most of the identity and dependency rules that decide whether a store causes re-render storms are hook rules: the React hooks cheatsheet covers every signature and the gotcha attached to it, and the array methods cheatsheet covers the immutable updates a store needs.
The frontend framework layer maps the rest of the client stack, and React vs Svelte covers the case where the framework's own reactivity would have removed this question entirely.
For where the data behind that state comes from, the SaaS stack guide assembles the backend and API layers. More matchups are in the comparisons index.