Jul 2026 · Comparison · ~11 min read
Zustand vs Redux Toolkit — Which State Manager in 2026?
By Safdar Ali — frontend engineer, Bengaluru
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 componentsRTK 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 normsNumbers 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:
- Buy me a coffee at buymeacoffee.com/safdarali
- Subscribe to my YouTube channel — it's free; 70+ React & Next.js tutorials
Related reading
More guides on safdarali.in — same author, production-focused.
- Comparison
Tailwind CSS vs CSS Modules — What I Use in Production
Tailwind vs CSS Modules 2026 — side-by-side component, 8-criteria table, and what Safdar Ali ships in production.
Jun 2026Read article →
- FrameSnap
Free Video Thumbnail Generator Online — No Upload
Free video thumbnail generator online — extract frames from video without upload. Browser-based, no watermark, YouTube thumbnail from MP4 workflow.
Jun 2026Read article →