DEV Community

Cover image for Zustand vs Redux Toolkit — Which State Manager in 2026?
Safdar Ali
Safdar Ali

Posted on

Zustand vs Redux Toolkit — Which State Manager in 2026?

Global state still matters in 2026 — but most pages should not need it. When they do, the debate is zustand vs redux 2026: Redux Toolkit (RTK) is the enterprise default; Zustand is the minimal store juniors actually read. I have shipped both on dashboards in production. This article implements the same counter plus async user fetch in each, compares bundle size, and ends with what I pick for new repos.

Zustand — counter and async fetch

// store/useAppStore.ts
import { create } from "zustand";

type User = { id: string; name: string } | null;

type State = {
count: number;
user: User;
loading: boolean;
increment: () => void;
fetchUser: () => Promise<void>;
};

export const useAppStore = create<State>((set) => ({
count: 0,
user: null,
loading: false,
increment: () => set((s) => ({ count: s.count + 1 })),
fetchUser: async () => {
set({ loading: true });
const res = await fetch("/api/me");
const user = await res.json();
set({ user, loading: false });
},
}));

// components/CounterPanel.tsx
"use client";
import { useAppStore } from "@/store/useAppStore";

export function CounterPanel() {
const count = useAppStore((s) => s.count);
const increment = useAppStore((s) => s.increment);
return <button onClick={increment}>Count: {count}</button>;
}

No providers, no slices folder — one file, selective subscriptions via selectors. That is why Zustand spreads on small teams.

The async fetch above is intentionally boring — no thunk middleware, just async/await inside the store action. For error handling you extend with try/catch and an error field; for retries you either wrap fetch or move the request to TanStack Query. Zustand does not prescribe async patterns, which is freedom or chaos depending on team discipline.

Redux Toolkit — same features, more structure

// store/appSlice.ts
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";

export const fetchUser = createAsyncThunk("app/fetchUser", async () => {
const res = await fetch("/api/me");
return res.json();
});

const appSlice = createSlice({
name: "app",
initialState: { count: 0, user: null as null | { id: string; name: string }, loading: false },
reducers: {
increment: (state) => { state.count += 1; },
},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => { state.loading = true; })
.addCase(fetchUser.fulfilled, (state, action) => {
state.user = action.payload;
state.loading = false;
});
},
});

export const { increment } = appSlice.actions;
export default appSlice.reducer;

// app/providers.tsx + useSelector in components

RTK removes classic Redux boilerplate but still needs a store provider, typed hooks, and slice conventions — worth it when ten engineers touch the same state graph.

// components/UserPanel.tsx — RTK async usage
"use client";
import { useEffect } from "react";
import { useAppDispatch, useAppSelector } from "@/store/hooks";
import { fetchUser, increment } from "@/store/appSlice";

export function UserPanel() {
const dispatch = useAppDispatch();
const count = useAppSelector((s) => s.app.count);
const user = useAppSelector((s) => s.app.user);
const loading = useAppSelector((s) => s.app.loading);

useEffect(() => {
dispatch(fetchUser());
}, [dispatch]);

return (
<div>
<button onClick={() => dispatch(increment())}>Count: {count}</button>
{loading ? <p>Loading…</p> : <p>{user?.name ?? "Guest"}</p>}
</div>
);
}

Same UX as the Zustand panel — more files, clearer audit trail in Redux DevTools when a bug report says "count jumped to 99 after refresh." That traceability is why enterprise codebases keep RTK despite smaller alternatives.

Bundle size comparison (approximate, gzipped)

// Measured on a minimal Next.js 15 client chunk (2026, rough)
// zustand@5 alone ~ 1.2 kB gzip
// @reduxjs/toolkit + react-redux ~ 12–14 kB gzip
// Note: RTK buys DevTools, middleware patterns, large-team norms

Numbers vary with tree-shaking and what you import. For a marketing site with one modal flag, neither library may be necessary — React context or URL state is enough. For a data-heavy dashboard, 12 kB might be cheap compared to engineering consistency.

Before you optimise kilobytes, profile real user metrics — I document that workflow in Next.js performance case study. A 10 kB store library rarely matters next to an unvirtualised table or a chart library. Still, greenfield SPAs with tight mobile budgets in India often pick Zustand because every gram of JS counts on 4G.

10-criteria comparison table

Criteria Zustand Redux Toolkit
Learning curve Low Medium
Boilerplate Minimal Structured
DevTools Plugin available Excellent native
Middleware Custom, light Mature ecosystem
Async patterns You write it createAsyncThunk
Team scale Small–medium Medium–large
Next.js App Router Client-only store Client-only store
Selectors Inline functions reselect / createSelector
Testing Easy store reset Well-documented patterns
Hiring familiarity in India Growing fast Still very common

Before and after — prop drilling vs store

// BEFORE — theme passed through five layers
<Layout theme={theme} setTheme={setTheme}>
<Sidebar theme={theme} setTheme={setTheme}>
<Nav theme={theme} setTheme={setTheme} />

// AFTER — Zustand (or RTK) at leaves only
const theme = useAppStore((s) => s.theme);
const setTheme = useAppStore((s) => s.setTheme);

Do not reach for a global store because props are annoying once — reach when multiple distant trees share writable state that is not server data.

Server state is not Redux or Zustand

API lists, pagination, cache invalidation — use TanStack Query or Server Components + fetch with cache tags. I see teams stuff fetch results into Redux out of habit; that duplicates what Next.js already solves on public pages — see SSR vs SSG vs ISR.

My production setup

New dashboard in 2026: Zustand for UI chrome (sidebar, filters, wizard step). RTK when joining a legacy codebase that already exports slices and middleware. At my day job, the RTK codebase had time-travel debugging worth the bytes; greenfield internal tools get Zustand in under an hour.

Pair with RSC boundaries — stores are client-only; never import them into Server Components.

The single takeaway

Zustand for new small/medium apps; RTK for large coordinated teams. Measure bundle impact, but optimise for maintainability. Most state should stay local or on the server.

Related: useCallback vs useMemo. Contact.

If this helped you

I publish free tutorials and write-ups like this in my spare time — no paywall on the guides. If it saved you an afternoon of trial and error, you can support the work:

More guides on safdarali.in — same author, production-focused.

Top comments (0)