{"id":314,"date":"2026-06-11T05:00:38","date_gmt":"2026-06-11T05:00:38","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/zustand-vs-redux-the-state-library-i-actually-reach-for\/"},"modified":"2026-06-11T05:00:38","modified_gmt":"2026-06-11T05:00:38","slug":"zustand-vs-redux-the-state-library-i-actually-reach-for","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/zustand-vs-redux-the-state-library-i-actually-reach-for\/","title":{"rendered":"Zustand vs Redux: The State Library I Actually Reach For"},"content":{"rendered":"<p>Confession: I defended Redux for years. I had the folder structure memorized, the <code>createSlice<\/code> muscle memory, the little speech about why global state needs structure. Then I built a small dashboard with Zustand over a weekend, and most of that speech stopped making sense. I kept waiting to miss something. I didn&rsquo;t.<\/p>\n<p>This isn&rsquo;t a &ldquo;Redux is dead&rdquo; post. Redux is fine, and on a couple of my projects it&rsquo;s still the right call. But the question I get asked most often by people on my team is some version of &ldquo;do we actually need Redux for this?&rdquo; and the honest answer, most of the time, is no. So here&rsquo;s how I think about the two now, with real code, and where I draw the line.<\/p>\n<h2 id=\"the-boilerplate-that-finally-got-to-me\">The boilerplate that finally got to me<\/h2>\n<p>The thing about Redux isn&rsquo;t that any single piece is hard. It&rsquo;s that there are a lot of pieces, and you set them all up before you&rsquo;ve stored a single value. Modern Redux Toolkit cut this down a lot from the old <code>mapDispatchToProps<\/code> days, and credit where it&rsquo;s due. But a basic counter still looks like this:<\/p>\n<pre><code class=\"language-jsx\">\/\/ store.js \u2014 Redux Toolkit\nimport { configureStore, createSlice } from '@reduxjs\/toolkit'\n\nconst counterSlice = createSlice({\n  name: 'counter',\n  initialState: { value: 0 },\n  reducers: {\n    increment: (state) =&gt; { state.value += 1 },\n    addBy: (state, action) =&gt; { state.value += action.payload },\n  },\n})\n\nexport const { increment, addBy } = counterSlice.actions\nexport const store = configureStore({ reducer: { counter: counterSlice.reducer } })\n<\/code><\/pre>\n<p>And then the component side needs a Provider wrapped around your app, plus the two hooks:<\/p>\n<pre><code class=\"language-jsx\">import { Provider, useSelector, useDispatch } from 'react-redux'\nimport { store, increment } from '.\/store'\n\nfunction Counter() {\n  const value = useSelector((s) =&gt; s.counter.value)\n  const dispatch = useDispatch()\n  return &lt;button onClick={() =&gt; dispatch(increment())}&gt;{value}&lt;\/button&gt;\n}\n\nexport default function App() {\n  return (\n    &lt;Provider store={store}&gt;\n      &lt;Counter \/&gt;\n    &lt;\/Provider&gt;\n  )\n}\n<\/code><\/pre>\n<p>That&rsquo;s a counter. For one number. I&rsquo;m not saying it&rsquo;s wrong, the structure pays off on big apps, but it&rsquo;s a lot of ceremony for what most features actually need.<\/p>\n<h2 id=\"what-zustand-actually-is\">What Zustand actually is<\/h2>\n<p>Zustand is a small state library from the same people who make React Three Fiber. The whole idea: a store is a hook. You call <code>create<\/code>, hand it a function that returns your state and the functions that change it, and you get back a hook you use anywhere. No provider. No actions, dispatchers, or reducers as separate concepts. You can read the <a href=\"https:\/\/github.com\/pmndrs\/zustand\" rel=\"nofollow noopener\" target=\"_blank\">Zustand source and docs on GitHub<\/a> in about the time it takes to finish a coffee, which was the first sign I was going to like it.<\/p>\n<p>Here&rsquo;s the same counter:<\/p>\n<pre><code class=\"language-jsx\">\/\/ store.js \u2014 Zustand\nimport { create } from 'zustand'\n\nexport const useCounter = create((set) =&gt; ({\n  value: 0,\n  increment: () =&gt; set((s) =&gt; ({ value: s.value + 1 })),\n  addBy: (n) =&gt; set((s) =&gt; ({ value: s.value + n })),\n}))\n<\/code><\/pre>\n<p>And the component:<\/p>\n<pre><code class=\"language-jsx\">import { useCounter } from '.\/store'\n\nfunction Counter() {\n  const value = useCounter((s) =&gt; s.value)\n  const increment = useCounter((s) =&gt; s.increment)\n  return &lt;button onClick={increment}&gt;{value}&lt;\/button&gt;\n}\n<\/code><\/pre>\n<p>No Provider. The state and the functions that change it live in the same place. The first time I wrote this I genuinely went back to check I hadn&rsquo;t forgotten a setup step.<\/p>\n<h2 id=\"zustand-vs-redux-the-differences-that-actually-matter\">Zustand vs Redux: the differences that actually matter<\/h2>\n<p>People frame this as a size contest, and yes, Zustand is tiny while a full Redux setup is heavier. But bundle size isn&rsquo;t why I switched. Three differences changed how I work day to day.<\/p>\n<p>First, there&rsquo;s no provider and no action layer. In Redux, changing state means dispatching an action that a reducer handles. That indirection is genuinely useful when you want a strict audit trail of every change. Most of my features don&rsquo;t need that. They need to set a value and move on.<\/p>\n<p>Second, Zustand stores live outside React. The store is a plain object you can import and read in a utility function, a test, or an event handler, without a hook. That sounds minor until you&rsquo;ve tried to read Redux state outside a component and remembered you can&rsquo;t really do that cleanly.<\/p>\n<p>Third, Redux gives you something Zustand doesn&rsquo;t: a strong convention. On a team of eight, &ldquo;all state changes go through actions&rdquo; is a rule you can enforce, and Redux DevTools time-travel debugging is genuinely great when a bug only shows up after a specific sequence of dispatches. Zustand has a <a href=\"https:\/\/github.com\/pmndrs\/zustand\" rel=\"nofollow noopener\" target=\"_blank\">devtools middleware<\/a> too, but the discipline is on you. That&rsquo;s the trade. Less structure means less friction and also less guardrail.<\/p>\n<p>If you&rsquo;re weighing this against plain Context, by the way, that&rsquo;s a different question. Context is a dependency-injection tool, not a state manager, and using it for frequently-changing values re-renders everything under the provider. The <a href=\"https:\/\/react.dev\/reference\/react\/useContext\" rel=\"nofollow noopener\" target=\"_blank\">React docs on useContext<\/a> are clear that it isn&rsquo;t built for high-frequency updates, which is exactly where a real store earns its place.<\/p>\n<h2 id=\"selectors-and-slices-the-part-people-get-wrong\">Selectors and slices: the part people get wrong<\/h2>\n<p>The mistake I see most with Zustand is people pulling the whole store into a component:<\/p>\n<pre><code class=\"language-jsx\">\/\/ re-renders on ANY store change, even unrelated ones\nconst store = useCounter()\n<\/code><\/pre>\n<p>Don&rsquo;t do that. Select the exact slice you need, and the component only re-renders when that slice changes:<\/p>\n<pre><code class=\"language-jsx\">\/\/ re-renders only when `value` changes\nconst value = useCounter((s) =&gt; s.value)\n<\/code><\/pre>\n<p>When you need a few fields at once, reach for <code>useShallow<\/code> so you don&rsquo;t trigger a re-render every time the store object identity changes:<\/p>\n<pre><code class=\"language-jsx\">import { useShallow } from 'zustand\/react\/shallow'\n\nconst { value, increment } = useCounter(\n  useShallow((s) =&gt; ({ value: s.value, increment: s.increment })),\n)\n<\/code><\/pre>\n<p>For bigger stores, split state into slices and compose them. This is the closest Zustand gets to Redux&rsquo;s structure, and it scales further than people expect:<\/p>\n<pre><code class=\"language-jsx\">const createFishSlice = (set) =&gt; ({\n  fishes: 0,\n  addFish: () =&gt; set((s) =&gt; ({ fishes: s.fishes + 1 })),\n})\n\nconst createBearSlice = (set) =&gt; ({\n  bears: 0,\n  addBear: () =&gt; set((s) =&gt; ({ bears: s.bears + 1 })),\n})\n\nexport const useStore = create((...a) =&gt; ({\n  ...createFishSlice(...a),\n  ...createBearSlice(...a),\n}))\n<\/code><\/pre>\n<p>The other thing I reach for constantly is the persist middleware. Saving state to localStorage in Redux usually means wiring up a subscriber or pulling in another package. In Zustand it&rsquo;s a wrapper around the store you already wrote, and it just works:<\/p>\n<pre><code class=\"language-jsx\">import { create } from 'zustand'\nimport { persist } from 'zustand\/middleware'\n\nexport const useSettings = create(\n  persist(\n    (set) =&gt; ({\n      theme: 'light',\n      setTheme: (theme) =&gt; set({ theme }),\n    }),\n    { name: 'app-settings' },\n  ),\n)\n<\/code><\/pre>\n<p>That&rsquo;s the whole thing. Theme survives a refresh, and I didn&rsquo;t have to think about serialization or hydration timing for the common case. The first time I needed persisted state in a Redux app it took me most of a morning and a library I later regretted adding.<\/p>\n<p>One thing worth saying clearly: this is client state. For data that lives on a server, neither Zustand nor Redux is the right tool, and I wrote about why in my post on <a href=\"https:\/\/abrarqasim.com\/blog\/tanstack-query-2026-what-i-reach-for-instead-of-useeffect\" rel=\"noopener\">why I reach for TanStack Query instead of useEffect<\/a>. Mixing the two, server cache in a global store, is how state management gets a bad name.<\/p>\n<h2 id=\"when-i-still-reach-for-redux\">When I still reach for Redux<\/h2>\n<p>I want to push back on my own enthusiasm here, because &ldquo;just use Zustand&rdquo; is the kind of hype I usually complain about.<\/p>\n<p>I still pick Redux when the app&rsquo;s whole value is the state itself. Think a complex editor, a trading view, anything where you&rsquo;ll want to replay actions, undo and redo across many entities, or hand a new engineer a predictable mental model on day one. The action log isn&rsquo;t ceremony there. It&rsquo;s the feature. Redux Toolkit&rsquo;s <a href=\"https:\/\/redux-toolkit.js.org\/rtk-query\/overview\" rel=\"nofollow noopener\" target=\"_blank\">RTK Query<\/a> also folds data fetching into the same store, and if your team already lives in Redux, adding another library to learn is a real cost, not a footnote.<\/p>\n<p>What I&rsquo;ve stopped doing is reaching for Redux by default on every new project because it&rsquo;s what I knew. That habit cost me a lot of boilerplate over the years for apps that were really just holding a handful of values. I dig into more of these &ldquo;what I actually use&rdquo; calls in my <a href=\"https:\/\/abrarqasim.com\" rel=\"noopener\">work and writing<\/a>, since most of my opinions here came from getting it wrong first.<\/p>\n<h2 id=\"what-to-try-this-week\">What to try this week<\/h2>\n<p>Pick one feature in your app that uses Redux for genuinely simple state: a theme toggle, a modal&rsquo;s open or closed flag, a small filter. Rebuild just that one store in Zustand and wire up the two or three components that touch it. Don&rsquo;t migrate the whole app, that&rsquo;s a different decision with a real cost. Just get one slice working so the difference stops being abstract. An afternoon is plenty. By the end of it you&rsquo;ll have a much clearer sense of which side of the line your project actually sits on, which beats any blog post, including this one.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I defended Redux for years, then Zustand made most of the boilerplate disappear. Here&#8217;s how the two compare, with real code, and where I still pick Redux.<\/p>\n","protected":false},"author":2,"featured_media":313,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"I defended Redux for years, then Zustand made most of the boilerplate disappear. Here's how the two compare, with real code, and where I still pick Redux.","rank_math_focus_keyword":"zustand vs redux","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[165,354],"tags":[38,41,206,356,205],"class_list":["post-314","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-javascript","category-react","tag-frontend","tag-react","tag-redux","tag-state-management-2","tag-zustand"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/314","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=314"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/314\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/313"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=314"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=314"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=314"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}