{"id":449,"date":"2026-07-12T05:05:14","date_gmt":"2026-07-12T05:05:14","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/zustand-vs-redux-2026-the-react-state-i-actually-reach-for\/"},"modified":"2026-07-12T05:05:14","modified_gmt":"2026-07-12T05:05:14","slug":"zustand-vs-redux-2026-the-react-state-i-actually-reach-for","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/zustand-vs-redux-2026-the-react-state-i-actually-reach-for\/","title":{"rendered":"Zustand vs Redux Toolkit in 2026: The State I Actually Ship"},"content":{"rendered":"<p>Confession: I spent about two hours last Tuesday arguing with a coworker about Zustand vs Redux Toolkit. We were both wrong in slightly different ways. He wanted to Redux-ify a tiny admin dashboard that had exactly one piece of shared state. I wanted to Zustand-ify a multi-tenant billing UI that had a genuinely complex reducer graph, undo\/redo, and three engineers touching it at once.<\/p>\n<p>We swapped opinions halfway through the argument, which is the kind of thing that only happens after too much coffee. Neither library is &ldquo;better.&rdquo; The interesting question is which one you should reach for on the app you&rsquo;re actually building this month.<\/p>\n<p>I&rsquo;ve now shipped both in production for a few years. This post is my honest map of which one I pick, when I pick the other one, and the specific moment I realized my defaults were wrong.<\/p>\n<h2 id=\"the-problem-i-keep-hitting-with-redux-toolkit\">The problem I keep hitting with Redux Toolkit<\/h2>\n<p>I have nothing against Redux Toolkit. It&rsquo;s the most polished version of Redux that has ever existed. <code>createSlice<\/code> fixed the most painful boilerplate, RTK Query is genuinely great for server state, and the DevTools are still the gold standard for time-travel debugging. If you&rsquo;re building anything with a large team and a long tail of features, RTK is a defensible pick.<\/p>\n<p>But here&rsquo;s the thing I keep running into on small-to-medium apps.<\/p>\n<p>Even after <code>createSlice<\/code>, you&rsquo;re still writing a Provider, a store file, typed hooks, and a slice per feature. For a modal-toggle-and-a-user-object app, that&rsquo;s a lot of ceremony. The old-way version, before I got tired of it, looked like this:<\/p>\n<pre><code class=\"language-jsx\">\/\/ store\/uiSlice.ts\nimport { createSlice, PayloadAction } from '@reduxjs\/toolkit'\n\nconst uiSlice = createSlice({\n  name: 'ui',\n  initialState: { isSidebarOpen: false, theme: 'light' as 'light' | 'dark' },\n  reducers: {\n    toggleSidebar: (state) =&gt; { state.isSidebarOpen = !state.isSidebarOpen },\n    setTheme: (state, action: PayloadAction&lt;'light' | 'dark'&gt;) =&gt; {\n      state.theme = action.payload\n    },\n  },\n})\n\nexport const { toggleSidebar, setTheme } = uiSlice.actions\nexport default uiSlice.reducer\n\n\/\/ store\/index.ts\nimport { configureStore } from '@reduxjs\/toolkit'\nimport ui from '.\/uiSlice'\nimport { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux'\n\nexport const store = configureStore({ reducer: { ui } })\nexport type RootState = ReturnType&lt;typeof store.getState&gt;\nexport type AppDispatch = typeof store.dispatch\nexport const useAppDispatch: () =&gt; AppDispatch = useDispatch\nexport const useAppSelector: TypedUseSelectorHook&lt;RootState&gt; = useSelector\n<\/code><\/pre>\n<p>Then a <code>&lt;Provider store={store}&gt;<\/code> in your root, and every component doing <code>const dispatch = useAppDispatch(); dispatch(toggleSidebar())<\/code> to flip a boolean.<\/p>\n<p>It&rsquo;s not offensive. It&rsquo;s just a lot of typing for one boolean.<\/p>\n<h2 id=\"zustands-whole-thing-in-about-eight-lines\">Zustand&rsquo;s whole thing in about eight lines<\/h2>\n<p>Zustand does the same job with less machinery. There&rsquo;s no Provider, no dispatch, no typed hooks helper. You define a store as a hook, and the hook is the store.<\/p>\n<pre><code class=\"language-jsx\">\/\/ store\/ui.ts\nimport { create } from 'zustand'\n\ntype UIState = {\n  isSidebarOpen: boolean\n  theme: 'light' | 'dark'\n  toggleSidebar: () =&gt; void\n  setTheme: (t: 'light' | 'dark') =&gt; void\n}\n\nexport const useUI = create&lt;UIState&gt;((set) =&gt; ({\n  isSidebarOpen: false,\n  theme: 'light',\n  toggleSidebar: () =&gt; set((s) =&gt; ({ isSidebarOpen: !s.isSidebarOpen })),\n  setTheme: (theme) =&gt; set({ theme }),\n}))\n<\/code><\/pre>\n<p>In a component:<\/p>\n<pre><code class=\"language-jsx\">const isSidebarOpen = useUI((s) =&gt; s.isSidebarOpen)\nconst toggleSidebar = useUI((s) =&gt; s.toggleSidebar)\n<\/code><\/pre>\n<p>That selector function on the hook is the important part. Zustand only re-renders the component when the slice you selected actually changes, so you get the &ldquo;only re-render what changed&rdquo; behavior for free without wrapping anything in <code>useMemo<\/code> or <code>React.memo<\/code>. Read the <a href=\"https:\/\/zustand.docs.pmnd.rs\/guides\/updating-state\" rel=\"nofollow noopener\" target=\"_blank\">Zustand docs on selectors<\/a> once and you&rsquo;ll internalize the pattern in about ten minutes.<\/p>\n<p>The whole library ships in a few kilobytes. The Redux Toolkit + React Redux pair is meaningfully bigger, and that adds up on mobile where JS parse time actually shows up in your Core Web Vitals.<\/p>\n<h2 id=\"where-zustand-actually-wins-and-its-not-what-people-say\">Where Zustand actually wins (and it&rsquo;s not what people say)<\/h2>\n<p>Everyone brings up bundle size and boilerplate. Fine, those matter. But the win I keep noticing is more subtle.<\/p>\n<p>The win is colocation. Zustand stores don&rsquo;t need to live in a top-level store file. I&rsquo;ve started putting feature-scoped stores in the feature folder next to the components that use them:<\/p>\n<pre><code>features\/\n  billing\/\n    BillingTable.tsx\n    useBillingStore.ts  &lt;-- lives here, not in \/store\n    edit-modal\/\n<\/code><\/pre>\n<p>That colocation is quietly wonderful. When we deleted the billing feature six months later, the store went with it. No dead code in a central <code>rootReducer<\/code>. No orphaned selectors. It just disappeared with the folder.<\/p>\n<p>You can do this with RTK if you&rsquo;re disciplined, but the library&rsquo;s shape actively pushes you toward a central store. Zustand&rsquo;s shape pushes you toward the store belonging to whatever needs it.<\/p>\n<p>The other quiet win is async. No thunks, no sagas, no listener middleware. If you want a fetch inside an action, write a fetch:<\/p>\n<pre><code class=\"language-jsx\">export const useBilling = create&lt;BillingState&gt;((set) =&gt; ({\n  invoices: [],\n  loading: false,\n  fetchInvoices: async () =&gt; {\n    set({ loading: true })\n    const res = await fetch('\/api\/invoices').then((r) =&gt; r.json())\n    set({ invoices: res, loading: false })\n  },\n}))\n<\/code><\/pre>\n<p>This is one of those things that sounds like a small thing until you&rsquo;ve written your fifth <code>createAsyncThunk<\/code> with three lifecycle actions and realized you were basically hand-rolling <code>useState<\/code> with extra steps.<\/p>\n<p>I wrote about a related shift in <a href=\"https:\/\/abrarqasim.com\/blog\/react-query-vs-swr-2026-how-i-actually-pick\" rel=\"noopener\">React Query vs SWR<\/a> \u2014 server state is a different animal from UI state, and once you separate those in your head, the &ldquo;state management&rdquo; question gets much smaller.<\/p>\n<h2 id=\"where-redux-toolkit-still-wins\">Where Redux Toolkit still wins<\/h2>\n<p>This is the section where I stop being a Zustand cheerleader.<\/p>\n<p>Time-travel DevTools are still the gold standard. Zustand has DevTools middleware and it works, but Redux DevTools with full action history is a different tier of debugging. On a support bot I was building last year, I could replay the last twenty user actions and see exactly where a state transition went sideways. That doesn&rsquo;t work as cleanly in Zustand, and if your app has a truly complex reducer graph, you&rsquo;ll miss it.<\/p>\n<p>RTK Query is a real feature. If you&rsquo;re going all-in on Redux for both UI and server state, RTK Query gives you caching, deduplication, and normalized results without adding another dependency. Zustand doesn&rsquo;t try to do server state. You&rsquo;d pair it with TanStack Query, which is fine, but that&rsquo;s two libraries instead of one.<\/p>\n<p>Bigger teams also appreciate the guardrails. Redux&rsquo;s whole idea, that actions are the only way state changes and every action is traceable, is more useful the more people are touching the code. On a team of two, &ldquo;here&rsquo;s a hook, call the function&rdquo; is fine. On a team of twelve rotating between features, &ldquo;every state change is a named, typed action that shows up in DevTools&rdquo; starts earning its keep.<\/p>\n<p>The <a href=\"https:\/\/redux-toolkit.js.org\/\" rel=\"nofollow noopener\" target=\"_blank\">official Redux Toolkit docs<\/a> are also much better now than they used to be. If you&rsquo;re picking Redux for the first time in 2026, follow their quickstart. Do not follow a 2018 blog post.<\/p>\n<h2 id=\"how-i-actually-pick-between-them-now\">How I actually pick between them now<\/h2>\n<p>My current heuristic, which I have not yet been embarrassed by:<\/p>\n<ul>\n<li><strong>Small app, one or two devs, mostly UI state:<\/strong> Zustand. Every time. The ceremony-to-value ratio isn&rsquo;t close.<\/li>\n<li><strong>Large app, five-plus devs, complex flows, need DevTools for support:<\/strong> RTK. The guardrails and tooling pay for the boilerplate.<\/li>\n<li><strong>Server state is the main thing you&rsquo;re managing:<\/strong> neither is the answer. Use TanStack Query for server state and Zustand (or <code>useState<\/code>) for whatever UI state is left over. This is the setup I reach for on 80% of new Next.js apps I start.<\/li>\n<li><strong>Migrating away from legacy Redux:<\/strong> RTK, not Zustand. A gradual <code>createSlice<\/code> migration is much lower risk than a full rewrite to a different mental model.<\/li>\n<\/ul>\n<p>Notice that &ldquo;which library has better performance&rdquo; is not on that list. In real apps, on real devices, the perf difference between a properly-configured Redux Toolkit setup and a properly-configured Zustand setup is inside the noise. If you&rsquo;re picking a state library for raw speed, you&rsquo;re optimizing the wrong thing.<\/p>\n<p>I share more of these opinionated dev calls in my <a href=\"https:\/\/abrarqasim.com\/about\" rel=\"noopener\">work notes on abrarqasim.com<\/a> if you want the running list.<\/p>\n<h2 id=\"the-one-migration-path-most-people-miss\">The one migration path most people miss<\/h2>\n<p>Here&rsquo;s the thing nobody told me until I did it wrong once.<\/p>\n<p>Zustand and Redux Toolkit can coexist in the same app. There is no rule that says you have to pick one. On the multi-tenant billing UI I mentioned at the top, we kept the existing RTK setup for the complex undo\/redo reducer graph and put all the new feature-local state in Zustand stores. Nothing broke. The two libraries do not fight over React&rsquo;s context or scheduling.<\/p>\n<p>That was the moment my defaults shifted. &ldquo;Which do I pick&rdquo; turned into &ldquo;which do I pick for this specific piece of state.&rdquo; Cheaper decisions, better outcomes.<\/p>\n<p>If you&rsquo;re staring at a legacy RTK setup and wishing you could Zustand-ify everything, don&rsquo;t. Zustand-ify one feature. See how it feels. Ship it. If you like it, do the next one. This is one of those situations where the incremental migration is the correct migration.<\/p>\n<h2 id=\"try-this-next-time-you-start-a-nextjs-app\">Try this next time you start a Next.js app<\/h2>\n<p>Concrete action for this week: on the next new Next.js project you spin up, don&rsquo;t add a state library at all in the first commit. Use <code>useState<\/code> and Server Components for as long as it works. When you actually need shared client state \u2014 usually around the point you&rsquo;re prop-drilling through three levels \u2014 reach for Zustand first. Only reach for RTK if you can name the specific feature it gives you that Zustand doesn&rsquo;t.<\/p>\n<p>You&rsquo;ll be surprised how often the answer is &ldquo;nothing, actually.&rdquo; Which is the correct answer more often than the internet suggests.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>After two years shipping both Zustand and Redux Toolkit in React apps, here&#8217;s how I actually decide, plus the migration path most teams miss.<\/p>\n","protected":false},"author":2,"featured_media":448,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"After two years shipping both Zustand and Redux Toolkit in React apps, here's how I actually decide, plus the migration path most teams miss.","rank_math_focus_keyword":"zustand vs redux","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[138,354],"tags":[38,44,61,41,355,206,497,356,63,205],"class_list":["post-449","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-frontend","category-react","tag-frontend","tag-javascript","tag-nextjs","tag-react","tag-react-hooks-2","tag-redux","tag-redux-toolkit-2","tag-state-management-2","tag-typescript","tag-zustand"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/449","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=449"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/449\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/448"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=449"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=449"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=449"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}