Zustand vs Redux Toolkit: Choosing React State in 2026

Photo by kevin dooley on flickr
For most new projects with no existing Redux investment, Zustand is the more pragmatic default because it needs no Provider, has far less boilerplate, and ships a much smaller bundle. Redux Toolkit still wins on large teams that need enforced structure, mature time-travel devtools, and a built-in data-fetching layer through RTK Query.
Redux Toolkit follows the classic Redux architecture, where a single store is created and then made available to the component tree through React context, which is what the Provider component sets up. Zustand sidesteps this entirely by exposing the store as a plain hook that any component can import and call directly, with no context wrapping required.
Yes, and this is the most common production pattern. TanStack Query owns anything fetched from an API - lists, records, pagination state - while Zustand owns state that only exists in the browser, like modal visibility or a multi-step form. Mixing the two responsibilities inside one store usually leads to stale or duplicated data.
Zustand's core package has no required dependencies beyond React, keeping its footprint minimal. Redux Toolkit bundles Immer and Redux Thunk by default, and a typical production setup adds react-redux for component bindings on top of that, making Redux Toolkit meaningfully heavier before any RTK Query data-fetching code is added.
Skip both if the state in question is only needed by a small, closely related part of your component tree - a parent component holding state in useState and passing it down as props is simpler and easier to reason about. Also skip both if the state actually came from an API response, since that belongs in a dedicated caching layer like TanStack Query rather than a global client store.

Photo by kevin dooley on flickr
Key Takeaway
For new React projects in 2026, Zustand is the sensible default: no Provider, a four-line store, and a tiny bundle. Redux Toolkit still earns its place on larger teams needing enforced slice structure, mature time-travel devtools, and RTK Query. Either way, server data belongs in TanStack Query, not a global client store.
Every few months a new thread asks whether Redux is dead, and every few months the answer is the same: no, but the default choice for a fresh React project in 2026 is rarely Redux Toolkit anymore. Zustand has become the pragmatic starting point for most teams, while Redux Toolkit remains the right call once an application grows past a certain size and needs enforced structure across many contributors.
This post walks through the real differences that matter day to day: how much code each library asks you to write, what debugging actually looks like with each one's devtools, how much weight they add to your bundle, and the mistake I see most often - reaching for either library to hold data that came from an API and belongs in a caching layer like TanStack Query instead. None of this is about which library is objectively superior; it is about matching the tool to the size and shape of the problem you actually have.
Zustand's starting point is a plain function that returns state and the functions that update it. There is no Provider to wrap your app in, no action types to define, and no dispatch call at the call site - components just call the store like a hook and read the slice of state they need.
// Zustand: a full store in ~10 lines, no Provider
import { create } from "zustand";
interface CartState {
items: string[];
addItem: (item: string) => void;
clear: () => void;
}
export const useCartStore = create<CartState>((set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
clear: () => set({ items: [] }),
}));
// Usage in a component, no wrapping context needed
function CartBadge() {
const items = useCartStore((state) => state.items);
return <span>{items.length}</span>;
}Redux Toolkit's createSlice function collapsed most of the ceremony that made classic Redux painful - you no longer hand write action types, action creators, or switch statements, and Immer lets you write mutative-looking code that is converted into immutable updates behind the scenes. That said, you still assemble a slice, register it with configureStore, and wrap your component tree in a Provider before any component can read from it. For a small feature, that is meaningfully more code than a single Zustand store.
// Redux Toolkit: createSlice cuts classic Redux boilerplate,
// but you still need a slice, a typed store, and Provider wiring
import { createSlice, configureStore } from "@reduxjs/toolkit";
const cartSlice = createSlice({
name: "cart",
initialState: { items: [] as string[] },
reducers: {
addItem: (state, action) => {
state.items.push(action.payload);
},
clear: (state) => {
state.items = [];
},
},
});
export const { addItem, clear } = cartSlice.actions;
export const store = configureStore({ reducer: { cart: cartSlice.reducer } });
// App root must be wrapped in <Provider store={store}>Both libraries plug into the same Redux DevTools browser extension, but the depth of that integration differs. Redux Toolkit's configureStore wires up the extension automatically with full support for time travel, action replay, and state diffing between every dispatched action.
If your team leans on time-travel debugging to reproduce production bugs from exported state dumps, that workflow is far more mature in Redux Toolkit. Zustand's devtools middleware gives you visibility, not a full replay pipeline.
Zustand's core package is tiny and ships with zero required dependencies beyond React itself. Redux Toolkit bundles configureStore, createSlice, Immer, and Redux Thunk together, and you almost always add react-redux on top to bind it to components. RTK Query, if you use it, is an additional package layered on top of that.
| Library | Core package | Typical production setup |
|---|---|---|
| Zustand | Minimal core, no required peer packages beyond React | Core store plus optional devtools and persist middleware |
| Redux Toolkit | Bundles Immer and Redux Thunk by default | Redux Toolkit plus react-redux for component bindings |
| Redux Toolkit + RTK Query | Adds a data fetching and caching layer on top | Full setup when RTK Query replaces manual fetch logic |
The most common architectural mistake I see in code review has nothing to do with which library you pick - it is putting server data inside a global client state store at all. Data fetched from an API is remote, asynchronous, and can be changed by other users at any time, which makes it fundamentally different from local UI state like a form value or a sidebar toggle. Teams that store fetched records in a Zustand or Redux slice usually end up hand-rolling their own loading flags, retry logic, and cache invalidation, which is exactly the work a dedicated data-fetching library already does correctly.
TanStack Query's own documentation draws this line explicitly: traditional state management libraries work well for client state but are not built to handle caching, background refetching, deduplication, or staleness for server data. That is a distinct problem with a distinct set of solutions, and trying to solve it by hand inside a Zustand store or a Redux slice usually means rebuilding a worse version of what a dedicated caching library already does.
The practical split that works well in production: TanStack Query owns anything that came from a fetch call - lists, records, pagination cursors - and Zustand or Redux Toolkit owns everything that lives only in the browser, like modal visibility, multi-step form state, or a theme preference.
Redux Toolkit even ships its own answer to this problem in RTK Query, which is deliberately built as a separate concern layered on top of the store rather than mixed into ordinary slices. That design choice by the Redux team itself is a strong signal that server state and client state deserve different tools, no matter which client-state library you end up choosing.
Before reaching for either library, it is worth asking whether you need global state at all. Three checks I run through with teams before adding a dependency:
A useContext plus useState pair, scoped to the part of the tree that actually needs it, is often the right answer for a single page or a single feature. Reach for Zustand or Redux Toolkit when state genuinely needs to be read and written from unrelated branches of the component tree.
For a new project starting today with no existing Redux investment, Zustand is the sensible default. The tiny bundle, the lack of a Provider requirement, and the four-line store definition mean less code to maintain and a lower barrier for new contributors to understand where state lives.
Redux Toolkit earns its place on larger teams and larger codebases, where the enforced structure of slices, the mature devtools with full time-travel, and RTK Query's opinionated data-fetching layer act as guardrails once dozens of engineers are touching the same store. It trades some ceremony for consistency, and on a big enough team that trade is worth making. The extra structure also pays off during onboarding: a new hire reading a Redux Toolkit codebase can predict where any given piece of state lives just by knowing the slice naming convention, which is harder to enforce across a large set of independently created Zustand stores without an additional convention layered on top by the team itself.
Whichever you pick, resist the urge to migrate an existing, working store just because a newer library is trending. Migrations carry real risk for marginal benefit unless the current setup is causing measurable pain - slow onboarding, frequent state bugs, or a bundle size that is actually hurting load time metrics.