Skip to main content

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 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

CriteriaZustandRedux Toolkit
Learning curveLowMedium
BoilerplateMinimalStructured
DevToolsPlugin availableExcellent native
MiddlewareCustom, lightMature ecosystem
Async patternsYou write itcreateAsyncThunk
Team scaleSmall–mediumMedium–large
Next.js App RouterClient-only storeClient-only store
SelectorsInline functionsreselect / createSelector
TestingEasy store resetWell-documented patterns
Hiring familiarity in IndiaGrowing fastStill 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.

"Talk is cheap. Show me the code."