[{"data":1,"prerenderedAt":1255},["ShallowReactive",2],{"article/zustand-vs-redux-toolkit":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"featured":6,"author":10,"categories":11,"slug":12,"image":13,"imageAlt":17,"published":18,"draft":6,"createdAt":19,"updatedAt":19,"faqs":20,"body":36,"_type":1249,"_id":1250,"_source":1251,"_file":1252,"_stem":1253,"_extension":1254,"isInteractive":6,"interactiveConfig":-1},"/articles/react/zustand-vs-redux-toolkit","react",false,"","Zustand vs Redux Toolkit - Picking React State","A practical comparison of Zustand and Redux Toolkit covering boilerplate, devtools, middleware, scaling, and when each is the right pick.","Thomas Findlay","React, Javascript","zustand-vs-redux-toolkit",[14,15,16],"/images/articles/zustand-vs-redux-toolkit/zustand-vs-redux-toolkit-1920w.avif","/images/articles/zustand-vs-redux-toolkit/zustand-vs-redux-toolkit-1920w.webp","/images/articles/zustand-vs-redux-toolkit/zustand-vs-redux-toolkit-1920w.png","Side-by-side comparison of Zustand and Redux Toolkit state management approaches",true,"2026-05-20T00:00:00",[21,24,27,30,33],{"question":22,"answer":23},"Is Zustand a replacement for Redux Toolkit?","Not always. Zustand replaces Redux for client-side state when an app needs a small, hook-first store without reducers, actions, or a Provider. Redux Toolkit remains the better pick when the project depends on RTK Query, formal middleware like redux-saga, time-travel debugging in Redux DevTools, or a team already trained on Redux patterns.",{"question":25,"answer":26},"Which has better TypeScript support, Zustand or Redux Toolkit?","Both have good TypeScript support, but they differ in shape. Redux Toolkit infers reducer payload types from createSlice automatically and pairs with typed selectors via the configureStore RootState helper. Zustand needs a small amount of generic boilerplate at the create() call but infers everything inside the store after that. For inference inside the consuming component, the two are roughly equivalent.",{"question":28,"answer":29},"Does Zustand work with Redux DevTools?","Yes, via the devtools middleware from zustand/middleware. It exposes the store as a Redux-shaped state tree to the browser extension so actions and state changes can be inspected. Time-travel debugging works, although the action labels are less rich than what createSlice emits in Redux Toolkit by default.",{"question":31,"answer":32},"Can I use RTK Query without Redux Toolkit?","Not really. RTK Query ships as part of @reduxjs/toolkit and depends on the Redux store to hold its cache. If a project wants RTK Query's caching and codegen but does not want Redux for client state, the practical path is to keep the Redux store but write the client state in Zustand on the side. The more common alternative is TanStack Query paired with Zustand, which is what most non-Redux apps reach for.",{"question":34,"answer":35},"When should I migrate from Redux Toolkit to Zustand?","Migrate when the Redux store has shrunk to a few isolated slices that do not need middleware, when the team no longer uses Redux DevTools time travel, and when the app does not depend on RTK Query. If any one of those three is still load-bearing, the migration costs more than it saves.",{"type":37,"children":38,"toc":1228},"root",[39,62,67,74,79,385,410,415,421,426,433,438,447,459,464,472,483,504,510,515,523,532,552,560,569,574,582,591,596,604,613,618,625,634,646,651,657,662,697,724,784,789,795,807,820,826,831,850,855,861,866,918,952,973,978,984,989,1019,1024,1030,1035,1063,1068,1074,1079,1085,1090,1118,1130,1135,1141,1146,1174,1186,1192,1197,1202,1207,1212,1218,1223],{"type":40,"tag":41,"props":42,"children":43},"element","p",{},[44,47,53,55,60],{"type":45,"value":46},"text","Picking a React state library in 2026 usually narrows to two options: ",{"type":40,"tag":48,"props":49,"children":50},"em",{},[51],{"type":45,"value":52},"Zustand",{"type":45,"value":54}," or ",{"type":40,"tag":48,"props":56,"children":57},{},[58],{"type":45,"value":59},"Redux Toolkit",{"type":45,"value":61},". Zustand is the small, hook-first store with almost no ceremony. Redux Toolkit is the official, opinionated Redux setup with reducers, devtools, and RTK Query bundled in. If the app is small and isolated, Zustand wins on ergonomics. If RTK Query, formal middleware, or a team already fluent in Redux is in play, Redux Toolkit wins.",{"type":40,"tag":41,"props":63,"children":64},{},[65],{"type":45,"value":66},"Most comparisons of the two libraries treat the choice as a stylistic preference. It is not. The decision changes how data fetching is wired, how middleware composes, what devtools experience the team gets, and how much code needs to move when requirements shift. The sections below walk through where each library actually wins, where each one stops scaling, and what a migration in either direction looks like.",{"type":40,"tag":68,"props":69,"children":71},"h2",{"id":70},"at-a-glance-comparison",[72],{"type":45,"value":73},"At a glance comparison",{"type":40,"tag":41,"props":75,"children":76},{},[77],{"type":45,"value":78},"A quick table to anchor the rest of the article. Numbers are the most recent published values at the time of writing. Source notes are underneath.",{"type":40,"tag":80,"props":81,"children":82},"table",{},[83,105],{"type":40,"tag":84,"props":85,"children":86},"thead",{},[87],{"type":40,"tag":88,"props":89,"children":90},"tr",{},[91,97,101],{"type":40,"tag":92,"props":93,"children":94},"th",{},[95],{"type":45,"value":96},"Concern",{"type":40,"tag":92,"props":98,"children":99},{},[100],{"type":45,"value":52},{"type":40,"tag":92,"props":102,"children":103},{},[104],{"type":45,"value":59},{"type":40,"tag":106,"props":107,"children":108},"tbody",{},[109,154,172,198,216,234,277,307,325,343,367],{"type":40,"tag":88,"props":110,"children":111},{},[112,118,130],{"type":40,"tag":113,"props":114,"children":115},"td",{},[116],{"type":45,"value":117},"API surface",{"type":40,"tag":113,"props":119,"children":120},{},[121,128],{"type":40,"tag":122,"props":123,"children":125},"code",{"className":124},[],[126],{"type":45,"value":127},"create()",{"type":45,"value":129}," returns a hook",{"type":40,"tag":113,"props":131,"children":132},{},[133,139,141,147,148],{"type":40,"tag":122,"props":134,"children":136},{"className":135},[],[137],{"type":45,"value":138},"createSlice",{"type":45,"value":140},", ",{"type":40,"tag":122,"props":142,"children":144},{"className":143},[],[145],{"type":45,"value":146},"configureStore",{"type":45,"value":140},{"type":40,"tag":122,"props":149,"children":151},{"className":150},[],[152],{"type":45,"value":153},"Provider",{"type":40,"tag":88,"props":155,"children":156},{},[157,162,167],{"type":40,"tag":113,"props":158,"children":159},{},[160],{"type":45,"value":161},"Boilerplate per slice",{"type":40,"tag":113,"props":163,"children":164},{},[165],{"type":45,"value":166},"One function, ~10 lines",{"type":40,"tag":113,"props":168,"children":169},{},[170],{"type":45,"value":171},"Slice + reducer + selector + dispatch",{"type":40,"tag":88,"props":173,"children":174},{},[175,180,185],{"type":40,"tag":113,"props":176,"children":177},{},[178],{"type":45,"value":179},"Provider required",{"type":40,"tag":113,"props":181,"children":182},{},[183],{"type":45,"value":184},"No",{"type":40,"tag":113,"props":186,"children":187},{},[188,190,196],{"type":45,"value":189},"Yes (",{"type":40,"tag":122,"props":191,"children":193},{"className":192},[],[194],{"type":45,"value":195},"\u003CProvider store={store}>",{"type":45,"value":197},")",{"type":40,"tag":88,"props":199,"children":200},{},[201,206,211],{"type":40,"tag":113,"props":202,"children":203},{},[204],{"type":45,"value":205},"Devtools",{"type":40,"tag":113,"props":207,"children":208},{},[209],{"type":45,"value":210},"Optional middleware, Redux DevTools compatible",{"type":40,"tag":113,"props":212,"children":213},{},[214],{"type":45,"value":215},"Redux DevTools, action history, time travel",{"type":40,"tag":88,"props":217,"children":218},{},[219,224,229],{"type":40,"tag":113,"props":220,"children":221},{},[222],{"type":45,"value":223},"Data fetching",{"type":40,"tag":113,"props":225,"children":226},{},[227],{"type":45,"value":228},"None bundled. Pair with TanStack Query or fetch",{"type":40,"tag":113,"props":230,"children":231},{},[232],{"type":45,"value":233},"RTK Query bundled",{"type":40,"tag":88,"props":235,"children":236},{},[237,242,272],{"type":40,"tag":113,"props":238,"children":239},{},[240],{"type":45,"value":241},"Middleware ecosystem",{"type":40,"tag":113,"props":243,"children":244},{},[245,251,252,258,259,265,266],{"type":40,"tag":122,"props":246,"children":248},{"className":247},[],[249],{"type":45,"value":250},"persist",{"type":45,"value":140},{"type":40,"tag":122,"props":253,"children":255},{"className":254},[],[256],{"type":45,"value":257},"devtools",{"type":45,"value":140},{"type":40,"tag":122,"props":260,"children":262},{"className":261},[],[263],{"type":45,"value":264},"immer",{"type":45,"value":140},{"type":40,"tag":122,"props":267,"children":269},{"className":268},[],[270],{"type":45,"value":271},"subscribeWithSelector",{"type":40,"tag":113,"props":273,"children":274},{},[275],{"type":45,"value":276},"redux-saga, redux-observable, redux-persist, listener middleware",{"type":40,"tag":88,"props":278,"children":279},{},[280,285,295],{"type":40,"tag":113,"props":281,"children":282},{},[283],{"type":45,"value":284},"TypeScript inference",{"type":40,"tag":113,"props":286,"children":287},{},[288,290],{"type":45,"value":289},"Good, with a small generic at ",{"type":40,"tag":122,"props":291,"children":293},{"className":292},[],[294],{"type":45,"value":127},{"type":40,"tag":113,"props":296,"children":297},{},[298,300,305],{"type":45,"value":299},"Strong via ",{"type":40,"tag":122,"props":301,"children":303},{"className":302},[],[304],{"type":45,"value":138},{"type":45,"value":306}," payload inference",{"type":40,"tag":88,"props":308,"children":309},{},[310,315,320],{"type":40,"tag":113,"props":311,"children":312},{},[313],{"type":45,"value":314},"Bundle size (gzipped, core)",{"type":40,"tag":113,"props":316,"children":317},{},[318],{"type":45,"value":319},"~0.5 KB",{"type":40,"tag":113,"props":321,"children":322},{},[323],{"type":45,"value":324},"~13.6 KB",{"type":40,"tag":88,"props":326,"children":327},{},[328,333,338],{"type":40,"tag":113,"props":329,"children":330},{},[331],{"type":45,"value":332},"Weekly npm downloads (May 2026)",{"type":40,"tag":113,"props":334,"children":335},{},[336],{"type":45,"value":337},"~37M",{"type":40,"tag":113,"props":339,"children":340},{},[341],{"type":45,"value":342},"~16M",{"type":40,"tag":88,"props":344,"children":345},{},[346,351,356],{"type":40,"tag":113,"props":347,"children":348},{},[349],{"type":45,"value":350},"Scaling pattern",{"type":40,"tag":113,"props":352,"children":353},{},[354],{"type":45,"value":355},"One store per concern, or slice pattern",{"type":40,"tag":113,"props":357,"children":358},{},[359,361],{"type":45,"value":360},"Feature slices combined by ",{"type":40,"tag":122,"props":362,"children":364},{"className":363},[],[365],{"type":45,"value":366},"combineReducers",{"type":40,"tag":88,"props":368,"children":369},{},[370,375,380],{"type":40,"tag":113,"props":371,"children":372},{},[373],{"type":45,"value":374},"Learning curve",{"type":40,"tag":113,"props":376,"children":377},{},[378],{"type":45,"value":379},"Hours",{"type":40,"tag":113,"props":381,"children":382},{},[383],{"type":45,"value":384},"Days",{"type":40,"tag":41,"props":386,"children":387},{},[388,390,399,401,408],{"type":45,"value":389},"Bundle sizes pulled from ",{"type":40,"tag":391,"props":392,"children":396},"a",{"href":393,"rel":394},"https://bundlephobia.com/package/zustand",[395],"nofollow",[397],{"type":45,"value":398},"Bundlephobia",{"type":45,"value":400}," (Zustand 5.0.13: 486 B gzipped; @reduxjs/toolkit 2.12.0: 13.6 KB gzipped). Weekly download counts pulled from the ",{"type":40,"tag":391,"props":402,"children":405},{"href":403,"rel":404},"https://api.npmjs.org/downloads/point/last-week/zustand",[395],[406],{"type":45,"value":407},"npm registry downloads API",{"type":45,"value":409}," for the week of 13–19 May 2026.",{"type":40,"tag":41,"props":411,"children":412},{},[413],{"type":45,"value":414},"The table sets up the trade-off. Zustand is dramatically smaller and faster to adopt. Redux Toolkit ships more in the box, including the data layer.",{"type":40,"tag":68,"props":416,"children":418},{"id":417},"same-slice-two-libraries",[419],{"type":45,"value":420},"Same slice, two libraries",{"type":40,"tag":41,"props":422,"children":423},{},[424],{"type":45,"value":425},"Theory only gets you so far. The same todo store written in both libraries shows where the real differences sit.",{"type":40,"tag":427,"props":428,"children":430},"h3",{"id":429},"the-zustand-version",[431],{"type":45,"value":432},"The Zustand version",{"type":40,"tag":41,"props":434,"children":435},{},[436],{"type":45,"value":437},"We start with Zustand. The store is a single function call. Actions and state live in the same object.",{"type":40,"tag":41,"props":439,"children":440},{},[441],{"type":40,"tag":442,"props":443,"children":444},"strong",{},[445],{"type":45,"value":446},"src/stores/todoStore.ts",{"type":40,"tag":448,"props":449,"children":454},"pre",{"className":450,"code":452,"language":453,"meta":7},[451],"language-typescript","import { create } from 'zustand'\n\ntype Todo = {\n  id: string\n  text: string\n  done: boolean\n}\n\ntype TodoState = {\n  todos: Todo[]\n  addTodo: (text: string) => void\n  toggleTodo: (id: string) => void\n}\n\nexport const useTodoStore = create\u003CTodoState>((set) => ({\n  todos: [],\n  addTodo: (text) =>\n    set((state) => ({\n      todos: [...state.todos, { id: crypto.randomUUID(), text, done: false }],\n    })),\n  toggleTodo: (id) =>\n    set((state) => ({\n      todos: state.todos.map((todo) =>\n        todo.id === id ? { ...todo, done: !todo.done } : todo\n      ),\n    })),\n}))\n","typescript",[455],{"type":40,"tag":122,"props":456,"children":457},{"__ignoreMap":7},[458],{"type":45,"value":452},{"type":40,"tag":41,"props":460,"children":461},{},[462],{"type":45,"value":463},"A consumer pulls exactly what it needs out of the hook.",{"type":40,"tag":41,"props":465,"children":466},{},[467],{"type":40,"tag":442,"props":468,"children":469},{},[470],{"type":45,"value":471},"src/components/TodoList.tsx",{"type":40,"tag":448,"props":473,"children":478},{"className":474,"code":476,"language":477,"meta":7},[475],"language-tsx","import { useTodoStore } from '../stores/todoStore'\n\nexport const TodoList = () => {\n  const todos = useTodoStore((state) => state.todos)\n  const toggleTodo = useTodoStore((state) => state.toggleTodo)\n\n  return (\n    \u003Cul>\n      {todos.map((todo) => (\n        \u003Cli key={todo.id} onClick={() => toggleTodo(todo.id)}>\n          {todo.done ? 'Done' : 'Todo'}: {todo.text}\n        \u003C/li>\n      ))}\n    \u003C/ul>\n  )\n}\n","tsx",[479],{"type":40,"tag":122,"props":480,"children":481},{"__ignoreMap":7},[482],{"type":45,"value":476},{"type":40,"tag":41,"props":484,"children":485},{},[486,488,494,496,502],{"type":45,"value":487},"That is the whole store and the whole consumer. There is no Provider, no dispatch, no separate selector layer. The hook is the API, and the selector function inside the hook controls re-renders. The component only re-renders when ",{"type":40,"tag":122,"props":489,"children":491},{"className":490},[],[492],{"type":45,"value":493},"state.todos",{"type":45,"value":495}," or the ",{"type":40,"tag":122,"props":497,"children":499},{"className":498},[],[500],{"type":45,"value":501},"toggleTodo",{"type":45,"value":503}," reference changes.",{"type":40,"tag":427,"props":505,"children":507},{"id":506},"the-redux-toolkit-version",[508],{"type":45,"value":509},"The Redux Toolkit version",{"type":40,"tag":41,"props":511,"children":512},{},[513],{"type":45,"value":514},"The same slice in Redux Toolkit takes more pieces. We need a slice file, a store file, a Provider at the app root, and typed hooks for the consumer.",{"type":40,"tag":41,"props":516,"children":517},{},[518],{"type":40,"tag":442,"props":519,"children":520},{},[521],{"type":45,"value":522},"src/store/todoSlice.ts",{"type":40,"tag":448,"props":524,"children":527},{"className":525,"code":526,"language":453,"meta":7},[451],"import { createSlice, PayloadAction } from '@reduxjs/toolkit'\n\ntype Todo = {\n  id: string\n  text: string\n  done: boolean\n}\n\ntype TodoState = {\n  todos: Todo[]\n}\n\nconst initialState: TodoState = { todos: [] }\n\nconst todoSlice = createSlice({\n  name: 'todos',\n  initialState,\n  reducers: {\n    addTodo: (state, action: PayloadAction\u003Cstring>) => {\n      state.todos.push({\n        id: crypto.randomUUID(),\n        text: action.payload,\n        done: false,\n      })\n    },\n    toggleTodo: (state, action: PayloadAction\u003Cstring>) => {\n      const todo = state.todos.find((t) => t.id === action.payload)\n      if (todo) todo.done = !todo.done\n    },\n  },\n})\n\nexport const { addTodo, toggleTodo } = todoSlice.actions\nexport default todoSlice.reducer\n",[528],{"type":40,"tag":122,"props":529,"children":530},{"__ignoreMap":7},[531],{"type":45,"value":526},{"type":40,"tag":41,"props":533,"children":534},{},[535,537,542,544,550],{"type":45,"value":536},"The reducer uses ",{"type":40,"tag":48,"props":538,"children":539},{},[540],{"type":45,"value":541},"Immer",{"type":45,"value":543}," internally, which is why the ",{"type":40,"tag":122,"props":545,"children":547},{"className":546},[],[548],{"type":45,"value":549},"push",{"type":45,"value":551}," and the direct property assignment look like mutations but produce a new state object. Next, the slice needs to be wired into a store.",{"type":40,"tag":41,"props":553,"children":554},{},[555],{"type":40,"tag":442,"props":556,"children":557},{},[558],{"type":45,"value":559},"src/store/index.ts",{"type":40,"tag":448,"props":561,"children":564},{"className":562,"code":563,"language":453,"meta":7},[451],"import { configureStore } from '@reduxjs/toolkit'\nimport todoReducer from './todoSlice'\n\nexport const store = configureStore({\n  reducer: {\n    todos: todoReducer,\n  },\n})\n\nexport type RootState = ReturnType\u003Ctypeof store.getState>\nexport type AppDispatch = typeof store.dispatch\n",[565],{"type":40,"tag":122,"props":566,"children":567},{"__ignoreMap":7},[568],{"type":45,"value":563},{"type":40,"tag":41,"props":570,"children":571},{},[572],{"type":45,"value":573},"Typed hooks live next to the store so consumers do not have to type their selectors and dispatch every time.",{"type":40,"tag":41,"props":575,"children":576},{},[577],{"type":40,"tag":442,"props":578,"children":579},{},[580],{"type":45,"value":581},"src/store/hooks.ts",{"type":40,"tag":448,"props":583,"children":586},{"className":584,"code":585,"language":453,"meta":7},[451],"import { useDispatch, useSelector, type TypedUseSelectorHook } from 'react-redux'\nimport type { RootState, AppDispatch } from './index'\n\nexport const useAppDispatch: () => AppDispatch = useDispatch\nexport const useAppSelector: TypedUseSelectorHook\u003CRootState> = useSelector\n",[587],{"type":40,"tag":122,"props":588,"children":589},{"__ignoreMap":7},[590],{"type":45,"value":585},{"type":40,"tag":41,"props":592,"children":593},{},[594],{"type":45,"value":595},"The Provider wraps the app once, near the root.",{"type":40,"tag":41,"props":597,"children":598},{},[599],{"type":40,"tag":442,"props":600,"children":601},{},[602],{"type":45,"value":603},"src/main.tsx",{"type":40,"tag":448,"props":605,"children":608},{"className":606,"code":607,"language":477,"meta":7},[475],"import { Provider } from 'react-redux'\nimport { store } from './store'\n\ncreateRoot(document.getElementById('root')!).render(\n  \u003CProvider store={store}>\n    \u003CApp />\n  \u003C/Provider>\n)\n",[609],{"type":40,"tag":122,"props":610,"children":611},{"__ignoreMap":7},[612],{"type":45,"value":607},{"type":40,"tag":41,"props":614,"children":615},{},[616],{"type":45,"value":617},"And finally the consumer.",{"type":40,"tag":41,"props":619,"children":620},{},[621],{"type":40,"tag":442,"props":622,"children":623},{},[624],{"type":45,"value":471},{"type":40,"tag":448,"props":626,"children":629},{"className":627,"code":628,"language":477,"meta":7},[475],"import { useAppSelector, useAppDispatch } from '../store/hooks'\nimport { toggleTodo } from '../store/todoSlice'\n\nexport const TodoList = () => {\n  const todos = useAppSelector((state) => state.todos.todos)\n  const dispatch = useAppDispatch()\n\n  return (\n    \u003Cul>\n      {todos.map((todo) => (\n        \u003Cli key={todo.id} onClick={() => dispatch(toggleTodo(todo.id))}>\n          {todo.done ? 'Done' : 'Todo'}: {todo.text}\n        \u003C/li>\n      ))}\n    \u003C/ul>\n  )\n}\n",[630],{"type":40,"tag":122,"props":631,"children":632},{"__ignoreMap":7},[633],{"type":45,"value":628},{"type":40,"tag":41,"props":635,"children":636},{},[637,639,644],{"type":45,"value":638},"The consumer reads roughly the same as the Zustand one, but to get here we wrote five files instead of two. The slice file alone is fine. ",{"type":40,"tag":122,"props":640,"children":642},{"className":641},[],[643],{"type":45,"value":138},{"type":45,"value":645}," is genuinely concise compared to old-school Redux. What stings is the store file, the Provider, the typed-hooks file, and the implicit coupling between them. None of those exist in the Zustand version, because there is no global store object to configure and no context boundary to cross.",{"type":40,"tag":41,"props":647,"children":648},{},[649],{"type":45,"value":650},"Cleaner on the Zustand side. More structure on the Redux Toolkit side. The question is whether that structure pays for itself.",{"type":40,"tag":68,"props":652,"children":654},{"id":653},"where-the-structure-earns-its-keep",[655],{"type":45,"value":656},"Where the structure earns its keep",{"type":40,"tag":41,"props":658,"children":659},{},[660],{"type":45,"value":661},"Redux Toolkit's extra ceremony is not gratuitous. The structure exists because Redux exposes three things Zustand does not give you by default.",{"type":40,"tag":41,"props":663,"children":664},{},[665,667,672,674,679,681,687,689,695],{"type":45,"value":666},"The first is ",{"type":40,"tag":48,"props":668,"children":669},{},[670],{"type":45,"value":671},"action history",{"type":45,"value":673}," in Redux DevTools. Every dispatched action shows up as a labelled entry in the timeline, with a diff of the state before and after, plus time-travel playback. Zustand can plug into Redux DevTools via the ",{"type":40,"tag":122,"props":675,"children":677},{"className":676},[],[678],{"type":45,"value":257},{"type":45,"value":680}," middleware, and that gets most of the way there, but the action labels are less informative because Zustand does not have named action types. Every ",{"type":40,"tag":122,"props":682,"children":684},{"className":683},[],[685],{"type":45,"value":686},"set()",{"type":45,"value":688}," call shows up as an anonymous mutation unless you name it manually inside ",{"type":40,"tag":122,"props":690,"children":692},{"className":691},[],[693],{"type":45,"value":694},"devtools({ name: '...' }, set)",{"type":45,"value":696},".",{"type":40,"tag":41,"props":698,"children":699},{},[700,702,707,709,715,717,722],{"type":45,"value":701},"The second is ",{"type":40,"tag":48,"props":703,"children":704},{},[705],{"type":45,"value":706},"RTK Query",{"type":45,"value":708},". RTK Query is the data-fetching and caching layer that ships inside ",{"type":40,"tag":122,"props":710,"children":712},{"className":711},[],[713],{"type":45,"value":714},"@reduxjs/toolkit",{"type":45,"value":716},". It writes its cache into the Redux store, which is why it is coupled to Redux. If the app needs RTK Query, Zustand is not an alternative. They solve different problems. The realistic comparison there is RTK Query versus ",{"type":40,"tag":48,"props":718,"children":719},{},[720],{"type":45,"value":721},"TanStack Query",{"type":45,"value":723}," paired with Zustand for client state.",{"type":40,"tag":41,"props":725,"children":726},{},[727,729,734,736,741,742,747,748,753,754,759,761,767,768,774,776,782],{"type":45,"value":728},"The third is the ",{"type":40,"tag":48,"props":730,"children":731},{},[732],{"type":45,"value":733},"middleware ecosystem",{"type":45,"value":735},". Redux has fifteen years of middleware behind it: redux-saga for complex async flows, redux-observable for RxJS-based effects, redux-persist for storage, the official listener middleware for side-effects, redux-undo for history, and so on. Zustand has a handful of first-party middlewares (",{"type":40,"tag":122,"props":737,"children":739},{"className":738},[],[740],{"type":45,"value":250},{"type":45,"value":140},{"type":40,"tag":122,"props":743,"children":745},{"className":744},[],[746],{"type":45,"value":257},{"type":45,"value":140},{"type":40,"tag":122,"props":749,"children":751},{"className":750},[],[752],{"type":45,"value":264},{"type":45,"value":140},{"type":40,"tag":122,"props":755,"children":757},{"className":756},[],[758],{"type":45,"value":271},{"type":45,"value":760},", plus ",{"type":40,"tag":122,"props":762,"children":764},{"className":763},[],[765],{"type":45,"value":766},"combine",{"type":45,"value":140},{"type":40,"tag":122,"props":769,"children":771},{"className":770},[],[772],{"type":45,"value":773},"redux",{"type":45,"value":775},", and ",{"type":40,"tag":122,"props":777,"children":779},{"className":778},[],[780],{"type":45,"value":781},"ssrSafe",{"type":45,"value":783},") and a thin layer of community ones, but the ecosystem is an order of magnitude smaller. For most apps that is fine. For apps that genuinely need saga-style orchestration or RxJS pipelines, Redux still wins.",{"type":40,"tag":41,"props":785,"children":786},{},[787],{"type":45,"value":788},"That is the trade-off in one sentence. Redux Toolkit gives you a wider ecosystem at the cost of more setup. Zustand gives you faster setup at the cost of a smaller ecosystem.",{"type":40,"tag":68,"props":790,"children":792},{"id":791},"where-zustand-stops-scaling",[793],{"type":45,"value":794},"Where Zustand stops scaling",{"type":40,"tag":41,"props":796,"children":797},{},[798,800,805],{"type":45,"value":799},"The first place Zustand strains is when a state change needs to coordinate side-effects across multiple slices. Zustand has ",{"type":40,"tag":122,"props":801,"children":803},{"className":802},[],[804],{"type":45,"value":271},{"type":45,"value":806}," for reacting to state changes outside React, and that solves most cases, but it does not give you the orchestrated, replayable, testable effect model that redux-saga or the Redux listener middleware does. If the app has a workflow like \"when the user logs out, clear seven different stores, cancel three in-flight requests, and persist a final analytics event in order\", Zustand can do it, but you will be hand-rolling the orchestration. Redux Toolkit with the listener middleware models that workflow declaratively.",{"type":40,"tag":41,"props":808,"children":809},{},[810,812,818],{"type":45,"value":811},"The second place is debugging. With Zustand, the devtools middleware gives you state inspection, but the action history is thin because Zustand has no named actions by default. When a bug reproduces by clicking through ten different actions, the Redux DevTools timeline in a Redux Toolkit app shows you exactly which ten actions, in which order, with which payloads. The Zustand timeline shows you ten anonymous ",{"type":40,"tag":122,"props":813,"children":815},{"className":814},[],[816],{"type":45,"value":817},"setState",{"type":45,"value":819}," entries unless every action was wrapped manually. For small apps this does not matter. For an app with eighty-plus components and a dozen interlocking slices, the debugging gap is real.",{"type":40,"tag":68,"props":821,"children":823},{"id":822},"where-redux-toolkit-is-overkill",[824],{"type":45,"value":825},"Where Redux Toolkit is overkill",{"type":40,"tag":41,"props":827,"children":828},{},[829],{"type":45,"value":830},"The reverse failure mode also exists. Redux Toolkit is overkill when an app has one or two pieces of client state (a sidebar open/closed flag, a UI theme, a small set of filters) and no need for RTK Query or formal middleware. Adding a Provider, a configured store, typed hooks, and a slice file for a boolean is exactly the kind of ceremony that makes new contributors roll their eyes.",{"type":40,"tag":41,"props":832,"children":833},{},[834,836,841,843,848],{"type":45,"value":835},"It is also overkill for component-tree-scoped state that does not need to live globally. ",{"type":40,"tag":48,"props":837,"children":838},{},[839],{"type":45,"value":840},"useState",{"type":45,"value":842}," and ",{"type":40,"tag":48,"props":844,"children":845},{},[846],{"type":45,"value":847},"useReducer",{"type":45,"value":849}," are still the right answer for state that belongs to one screen. Reaching for any global store, Zustand or Redux Toolkit, when the data does not cross component-tree boundaries is the most common state-management mistake I see in code reviews. The library is a hammer, but the problem is not always a nail.",{"type":40,"tag":41,"props":851,"children":852},{},[853],{"type":45,"value":854},"When the global store is genuinely warranted and the requirements are modest, Zustand fits the bill with less code. When the requirements grow into the territory of cache invalidation, optimistic updates, polling, or multi-slice orchestration, Redux Toolkit's bundled features start paying back the cost of its setup.",{"type":40,"tag":68,"props":856,"children":858},{"id":857},"typescript-inference-side-by-side",[859],{"type":45,"value":860},"TypeScript inference, side by side",{"type":40,"tag":41,"props":862,"children":863},{},[864],{"type":45,"value":865},"Both libraries have respectable TypeScript stories, but the shape is different.",{"type":40,"tag":41,"props":867,"children":868},{},[869,871,876,878,884,886,892,894,900,902,908,910,916],{"type":45,"value":870},"Redux Toolkit's ",{"type":40,"tag":122,"props":872,"children":874},{"className":873},[],[875],{"type":45,"value":138},{"type":45,"value":877}," infers the payload type from the reducer signature. The action creator generated for ",{"type":40,"tag":122,"props":879,"children":881},{"className":880},[],[882],{"type":45,"value":883},"addTodo",{"type":45,"value":885}," is automatically typed as ",{"type":40,"tag":122,"props":887,"children":889},{"className":888},[],[890],{"type":45,"value":891},"(text: string) => PayloadAction\u003Cstring>",{"type":45,"value":893}," because the reducer declared ",{"type":40,"tag":122,"props":895,"children":897},{"className":896},[],[898],{"type":45,"value":899},"action: PayloadAction\u003Cstring>",{"type":45,"value":901},". The selector layer gets typed via the ",{"type":40,"tag":122,"props":903,"children":905},{"className":904},[],[906],{"type":45,"value":907},"RootState",{"type":45,"value":909}," helper, and the dispatch hook via ",{"type":40,"tag":122,"props":911,"children":913},{"className":912},[],[914],{"type":45,"value":915},"AppDispatch",{"type":45,"value":917},". Once those typed hooks are written, consumers get full inference for free.",{"type":40,"tag":41,"props":919,"children":920},{},[921,923,929,931,936,937,943,945,950],{"type":45,"value":922},"Zustand requires you to write the state interface once and pass it as a generic to ",{"type":40,"tag":122,"props":924,"children":926},{"className":925},[],[927],{"type":45,"value":928},"create\u003CTodoState>()",{"type":45,"value":930},". Inside the store, every call to ",{"type":40,"tag":122,"props":932,"children":934},{"className":933},[],[935],{"type":45,"value":686},{"type":45,"value":842},{"type":40,"tag":122,"props":938,"children":940},{"className":939},[],[941],{"type":45,"value":942},"get()",{"type":45,"value":944}," is typed against that interface. Outside the store, the hook is already typed. The pattern is slightly more manual at the definition site but slightly less verbose at the consumer site, because there is no separate ",{"type":40,"tag":122,"props":946,"children":948},{"className":947},[],[949],{"type":45,"value":907},{"type":45,"value":951}," indirection and no typed selector helpers to wire.",{"type":40,"tag":41,"props":953,"children":954},{},[955,957,963,965,971],{"type":45,"value":956},"A practical observation: the friction shows up most when generics are inferred through middleware. Zustand's middleware composition (",{"type":40,"tag":122,"props":958,"children":960},{"className":959},[],[961],{"type":45,"value":962},"create\u003CState>()(devtools(persist((set) => ({ ... }))))",{"type":45,"value":964},") used to require manual generic threading. Recent versions (Zustand 4.0 onwards, when the curried ",{"type":40,"tag":122,"props":966,"children":968},{"className":967},[],[969],{"type":45,"value":970},"create\u003CT>()(...)",{"type":45,"value":972}," form landed) have improved the inference, but the type errors when something is wrong are still less helpful than the Redux Toolkit equivalents.",{"type":40,"tag":41,"props":974,"children":975},{},[976],{"type":45,"value":977},"For a small store, the two are a wash. For a large store with three or four middlewares stacked, Redux Toolkit's type errors are usually easier to read.",{"type":40,"tag":68,"props":979,"children":981},{"id":980},"when-to-pick-zustand",[982],{"type":45,"value":983},"When to pick Zustand",{"type":40,"tag":41,"props":985,"children":986},{},[987],{"type":45,"value":988},"A handful of scenarios where Zustand is the right default:",{"type":40,"tag":990,"props":991,"children":992},"ul",{},[993,999,1004,1009,1014],{"type":40,"tag":994,"props":995,"children":996},"li",{},[997],{"type":45,"value":998},"The app is small or medium, and global state is one or two concerns (theme, sidebar, filters, modal stack).",{"type":40,"tag":994,"props":1000,"children":1001},{},[1002],{"type":45,"value":1003},"The team is using TanStack Query for server state and only needs a thin client-state layer alongside it.",{"type":40,"tag":994,"props":1005,"children":1006},{},[1007],{"type":45,"value":1008},"A library or design-system package needs its own internal store without forcing consumers to add a Provider.",{"type":40,"tag":994,"props":1010,"children":1011},{},[1012],{"type":45,"value":1013},"A team wants to start writing global state immediately without spending a week on Redux ergonomics first.",{"type":40,"tag":994,"props":1015,"children":1016},{},[1017],{"type":45,"value":1018},"Bundle size is a genuine constraint, like a content-heavy site or a mobile-targeted web app where every kilobyte counts.",{"type":40,"tag":41,"props":1020,"children":1021},{},[1022],{"type":45,"value":1023},"The library wins on time-to-first-store and on bundle size. The trade-off you accept is a smaller middleware ecosystem and less formality as the app grows.",{"type":40,"tag":68,"props":1025,"children":1027},{"id":1026},"when-to-pick-redux-toolkit",[1028],{"type":45,"value":1029},"When to pick Redux Toolkit",{"type":40,"tag":41,"props":1031,"children":1032},{},[1033],{"type":45,"value":1034},"Redux Toolkit is the right default in these cases:",{"type":40,"tag":990,"props":1036,"children":1037},{},[1038,1043,1048,1053,1058],{"type":40,"tag":994,"props":1039,"children":1040},{},[1041],{"type":45,"value":1042},"The app needs RTK Query and the bundled cache layer.",{"type":40,"tag":994,"props":1044,"children":1045},{},[1046],{"type":45,"value":1047},"The team is already trained on Redux patterns from a previous project, and re-training to a new mental model has a real cost.",{"type":40,"tag":994,"props":1049,"children":1050},{},[1051],{"type":45,"value":1052},"The app has complex async workflows that benefit from the listener middleware, redux-saga, or redux-observable.",{"type":40,"tag":994,"props":1054,"children":1055},{},[1056],{"type":45,"value":1057},"Action-level debugging, time travel, and replayable test fixtures are part of the engineering culture.",{"type":40,"tag":994,"props":1059,"children":1060},{},[1061],{"type":45,"value":1062},"The codebase will grow past twelve or fifteen feature slices and needs a single, enforced structure for state.",{"type":40,"tag":41,"props":1064,"children":1065},{},[1066],{"type":45,"value":1067},"Redux Toolkit's value compounds with app size and team size. Below a threshold, it is friction. Above the threshold, the friction pays for itself.",{"type":40,"tag":68,"props":1069,"children":1071},{"id":1070},"migration-considerations",[1072],{"type":45,"value":1073},"Migration considerations",{"type":40,"tag":41,"props":1075,"children":1076},{},[1077],{"type":45,"value":1078},"Teams asking the migration question usually fall into two camps. Each direction has a different cost profile.",{"type":40,"tag":427,"props":1080,"children":1082},{"id":1081},"migrating-from-redux-toolkit-to-zustand",[1083],{"type":45,"value":1084},"Migrating from Redux Toolkit to Zustand",{"type":40,"tag":41,"props":1086,"children":1087},{},[1088],{"type":45,"value":1089},"This is the more common direction in 2026. The trigger is usually that an existing Redux app has shrunk to a few isolated slices after server state was moved to TanStack Query or RTK Query was never adopted.",{"type":40,"tag":41,"props":1091,"children":1092},{},[1093,1095,1100,1102,1108,1110,1116],{"type":45,"value":1094},"The migration is mechanical for slices that do not use middleware. Each slice becomes a ",{"type":40,"tag":122,"props":1096,"children":1098},{"className":1097},[],[1099],{"type":45,"value":127},{"type":45,"value":1101}," call. Selectors map almost one-to-one. Redux Toolkit's ",{"type":40,"tag":122,"props":1103,"children":1105},{"className":1104},[],[1106],{"type":45,"value":1107},"useAppSelector((state) => state.todos.todos)",{"type":45,"value":1109}," becomes Zustand's ",{"type":40,"tag":122,"props":1111,"children":1113},{"className":1112},[],[1114],{"type":45,"value":1115},"useTodoStore((state) => state.todos)",{"type":45,"value":1117},". Dispatched actions become direct method calls on the store. The Provider goes away. Typed hooks go away.",{"type":40,"tag":41,"props":1119,"children":1120},{},[1121,1123,1128],{"type":45,"value":1122},"What does not migrate cleanly: anything that uses redux-saga, redux-observable, or the listener middleware. Those patterns assume the Redux dispatch pipeline. Reimplementing them in Zustand means rebuilding the orchestration by hand using ",{"type":40,"tag":122,"props":1124,"children":1126},{"className":1125},[],[1127],{"type":45,"value":271},{"type":45,"value":1129}," and async functions. If a slice depends on saga-style coordination, that slice is a poor migration candidate.",{"type":40,"tag":41,"props":1131,"children":1132},{},[1133],{"type":45,"value":1134},"RTK Query also does not migrate cleanly. If the app uses RTK Query, the migration is not from Redux Toolkit to Zustand. It is from RTK Query to TanStack Query first, then from Redux Toolkit to Zustand. That is two migrations, not one, and they should be planned separately. I would recommend doing the data-fetching swap first, letting it settle for a few weeks, and only then evaluating whether the remaining client state still needs Redux.",{"type":40,"tag":427,"props":1136,"children":1138},{"id":1137},"migrating-from-zustand-to-redux-toolkit",[1139],{"type":45,"value":1140},"Migrating from Zustand to Redux Toolkit",{"type":40,"tag":41,"props":1142,"children":1143},{},[1144],{"type":45,"value":1145},"The reverse migration is less common but happens when a Zustand codebase has outgrown its conventions. The triggers are usually inconsistent store patterns across features, debugging pain from anonymous actions, or a new requirement for RTK Query.",{"type":40,"tag":41,"props":1147,"children":1148},{},[1149,1151,1157,1159,1165,1167,1173],{"type":45,"value":1150},"The migration is more work because Redux Toolkit demands structure that Zustand does not. Each Zustand store becomes a slice. Each store's actions become reducer cases. The Provider gets added once at the root. Typed hooks get wired. Consumers shift from ",{"type":40,"tag":122,"props":1152,"children":1154},{"className":1153},[],[1155],{"type":45,"value":1156},"useStoreName(selector)",{"type":45,"value":1158}," to ",{"type":40,"tag":122,"props":1160,"children":1162},{"className":1161},[],[1163],{"type":45,"value":1164},"useAppSelector(selector)",{"type":45,"value":1166}," plus ",{"type":40,"tag":122,"props":1168,"children":1170},{"className":1169},[],[1171],{"type":45,"value":1172},"dispatch(action(payload))",{"type":45,"value":696},{"type":40,"tag":41,"props":1175,"children":1176},{},[1177,1179,1184],{"type":45,"value":1178},"The migration is mostly mechanical, but the diff is large. Plan it feature by feature, not all at once. Run both stores in parallel during the transition. Redux Toolkit's ",{"type":40,"tag":122,"props":1180,"children":1182},{"className":1181},[],[1183],{"type":45,"value":153},{"type":45,"value":1185}," and Zustand stores do not conflict, so a half-migrated codebase still works.",{"type":40,"tag":68,"props":1187,"children":1189},{"id":1188},"coupling-with-the-rest-of-the-stack",[1190],{"type":45,"value":1191},"Coupling with the rest of the stack",{"type":40,"tag":41,"props":1193,"children":1194},{},[1195],{"type":45,"value":1196},"State-management choices are not made in isolation. They couple with the data-fetching choice, the form-library choice, and the routing choice.",{"type":40,"tag":41,"props":1198,"children":1199},{},[1200],{"type":45,"value":1201},"Zustand pairs naturally with TanStack Query. The pattern is server state in TanStack Query, client state in Zustand, and the two layers do not need to know about each other. This is the dominant pattern in non-Redux React apps in 2026.",{"type":40,"tag":41,"props":1203,"children":1204},{},[1205],{"type":45,"value":1206},"Redux Toolkit pairs naturally with RTK Query, since they ship together and share the store. Server state in RTK Query, client state in Redux Toolkit slices, both inspected in the same Redux DevTools panel. The coupling is tighter, but the developer experience is cohesive.",{"type":40,"tag":41,"props":1208,"children":1209},{},[1210],{"type":45,"value":1211},"Mixing the two (Redux Toolkit for client state with TanStack Query for server state, for example) works but loses the cohesion. The team has to operate in two mental models simultaneously, and the devtools experience is split across two panels.",{"type":40,"tag":68,"props":1213,"children":1215},{"id":1214},"picking-finally",[1216],{"type":45,"value":1217},"Picking, finally",{"type":40,"tag":41,"props":1219,"children":1220},{},[1221],{"type":45,"value":1222},"Zustand or Redux Toolkit is the wrong question if it is asked in the abstract. The right version of the question is: what does the rest of the stack look like, how big will the app be, and what does the team already know?",{"type":40,"tag":41,"props":1224,"children":1225},{},[1226],{"type":45,"value":1227},"For a small app, a library, or a team that wants TanStack Query for data, pick Zustand. For an app that needs RTK Query, action-level debugging, or formal middleware, pick Redux Toolkit. For everything in between, the deciding factor is usually team familiarity rather than technical fit.",{"title":7,"searchDepth":1229,"depth":1229,"links":1230},2,[1231,1232,1237,1238,1239,1240,1241,1242,1243,1247,1248],{"id":70,"depth":1229,"text":73},{"id":417,"depth":1229,"text":420,"children":1233},[1234,1236],{"id":429,"depth":1235,"text":432},3,{"id":506,"depth":1235,"text":509},{"id":653,"depth":1229,"text":656},{"id":791,"depth":1229,"text":794},{"id":822,"depth":1229,"text":825},{"id":857,"depth":1229,"text":860},{"id":980,"depth":1229,"text":983},{"id":1026,"depth":1229,"text":1029},{"id":1070,"depth":1229,"text":1073,"children":1244},[1245,1246],{"id":1081,"depth":1235,"text":1084},{"id":1137,"depth":1235,"text":1140},{"id":1188,"depth":1229,"text":1191},{"id":1214,"depth":1229,"text":1217},"markdown","content:articles:react:zustand-vs-redux-toolkit.md","content","articles/react/zustand-vs-redux-toolkit.md","articles/react/zustand-vs-redux-toolkit","md",1779373153722]