[{"data":1,"prerenderedAt":1755},["ShallowReactive",2],{"article/rtk-query-vs-tanstack-query":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":39,"_type":1749,"_id":1750,"_source":1751,"_file":1752,"_stem":1753,"_extension":1754,"isInteractive":6,"interactiveConfig":-1},"/articles/react/rtk-query-vs-tanstack-query","react",false,"","RTK Query vs TanStack Query - Which to Pick","A practical comparison of RTK Query and TanStack Query covering caching, ergonomics, bundle size, and when each is the right pick.","Thomas Findlay","React, Javascript","rtk-query-vs-tanstack-query",[14,15,16],"/images/articles/rtk-query-vs-tanstack-query/rtk-query-vs-tanstack-query-1920w.avif","/images/articles/rtk-query-vs-tanstack-query/rtk-query-vs-tanstack-query-1920w.web","/images/articles/rtk-query-vs-tanstack-query/rtk-query-vs-tanstack-query-1920w.png","Side-by-side comparison of RTK Query and TanStack Query data fetching APIs",true,"2026-05-21T00:00:00",[21,24,27,30,33,36],{"question":22,"answer":23},"Which is better, RTK Query or TanStack Query?","Neither wins outright. Pick RTK Query if your app already runs on Redux Toolkit, because it reuses the store, middleware, and DevTools you've already set up. Pick TanStack Query if you don't want a Redux dependency, work across multiple frameworks, or need fine-grained control over query keys and refetching behaviour.",{"question":25,"answer":26},"Can I use TanStack Query with Redux?","Yes. The two coexist without conflict. TanStack Query keeps server state in its own cache, separate from Redux. Use Redux for client state (UI flags, forms, derived computations) and TanStack Query for anything that lives on a server. The split is cleaner than pushing everything through Redux.",{"question":28,"answer":29},"Does RTK Query support frameworks other than React?","No. RTK Query ships hooks for React only. Redux Toolkit itself is framework-agnostic, but the data-fetching hooks (useGetUsersQuery, useCreateUserMutation, and so on) are React-specific. If you need Vue, Svelte, Solid, or Angular bindings, TanStack Query is the choice.",{"question":31,"answer":32},"How do RTK Query and TanStack Query handle cache invalidation?","RTK Query uses tag-based invalidation. Queries declare providesTags, mutations declare invalidatesTags, and overlapping tags trigger refetches automatically. TanStack Query uses query keys. You call queryClient.invalidateQueries with a key (or key prefix), and any matching cached query is marked stale and refetched. Both models work. The tag system needs less manual orchestration once it's set up; query keys give you finer-grained control over what refetches.",{"question":34,"answer":35},"Is TanStack Query smaller than RTK Query?","The two have similar gzipped sizes, around 13.6 KB each. RTK Query, however, is bundled inside Redux Toolkit, which means you also pull in the Redux store and middleware. If you don't already need Redux, TanStack Query is the lighter overall footprint.",{"question":37,"answer":38},"Can I migrate from RTK Query to TanStack Query (or vice versa)?","You can migrate incrementally. Both libraries expose hooks per endpoint, so you can convert one feature at a time without touching the rest of the app. The bigger work is reshaping cache invalidation, because RTK Query's tag system doesn't map one-to-one onto TanStack Query's query keys, and vice versa.",{"type":40,"children":41,"toc":1722},"root",[42,65,72,77,83,370,375,381,386,393,405,414,426,447,452,460,469,474,482,493,535,541,560,567,576,581,588,597,609,614,620,625,631,641,667,677,683,693,710,759,765,770,776,781,787,816,842,851,871,883,889,916,936,945,965,993,999,1004,1041,1069,1074,1080,1085,1090,1095,1137,1158,1218,1224,1229,1234,1247,1252,1257,1262,1291,1342,1370,1376,1381,1432,1452,1457,1462,1468,1473,1483,1537,1578,1603,1609,1614,1683,1688,1694,1699,1704,1709],{"type":43,"tag":44,"props":45,"children":46},"element","p",{},[47,50,56,58,63],{"type":48,"value":49},"text","Once your React app is making more than a handful of API calls, the question of which data-fetching library to standardise on becomes hard to put off. Two libraries dominate that decision. ",{"type":43,"tag":51,"props":52,"children":53},"em",{},[54],{"type":48,"value":55},"RTK Query",{"type":48,"value":57},", bundled into Redux Toolkit, and ",{"type":43,"tag":51,"props":59,"children":60},{},[61],{"type":48,"value":62},"TanStack Query",{"type":48,"value":64},", the framework-agnostic successor to React Query. They solve the same core problems: caching, deduplication, background refetching, loading and error states. Where they differ is ergonomics, framework breadth, and how much of your existing stack they assume.",{"type":43,"tag":66,"props":67,"children":69},"h2",{"id":68},"tldr",[70],{"type":48,"value":71},"TL;DR",{"type":43,"tag":44,"props":73,"children":74},{},[75],{"type":48,"value":76},"If your app already uses Redux Toolkit, pick RTK Query. You'll get data fetching that plugs into your store, middleware, and DevTools without adding a second cache or a second mental model. If you don't have Redux, work across multiple frameworks, or want the lighter overall footprint, pick TanStack Query. Both libraries are good. The right choice depends almost entirely on what's already in your stack.",{"type":43,"tag":66,"props":78,"children":80},{"id":79},"comparison-at-a-glance",[81],{"type":48,"value":82},"Comparison at a glance",{"type":43,"tag":84,"props":85,"children":86},"table",{},[87,109],{"type":43,"tag":88,"props":89,"children":90},"thead",{},[91],{"type":43,"tag":92,"props":93,"children":94},"tr",{},[95,101,105],{"type":43,"tag":96,"props":97,"children":98},"th",{},[99],{"type":48,"value":100},"Dimension",{"type":43,"tag":96,"props":102,"children":103},{},[104],{"type":48,"value":55},{"type":43,"tag":96,"props":106,"children":107},{},[108],{"type":48,"value":62},{"type":43,"tag":110,"props":111,"children":112},"tbody",{},[113,164,201,219,237,265,298,316,334,352],{"type":43,"tag":92,"props":114,"children":115},{},[116,122,144],{"type":43,"tag":117,"props":118,"children":119},"td",{},[120],{"type":48,"value":121},"Caching model",{"type":43,"tag":117,"props":123,"children":124},{},[125,127,134,136,142],{"type":48,"value":126},"Tag-based (",{"type":43,"tag":128,"props":129,"children":131},"code",{"className":130},[],[132],{"type":48,"value":133},"providesTags",{"type":48,"value":135}," / ",{"type":43,"tag":128,"props":137,"children":139},{"className":138},[],[140],{"type":48,"value":141},"invalidatesTags",{"type":48,"value":143},")",{"type":43,"tag":117,"props":145,"children":146},{},[147,149,155,157,163],{"type":48,"value":148},"Query-key-based (",{"type":43,"tag":128,"props":150,"children":152},{"className":151},[],[153],{"type":48,"value":154},"queryKey",{"type":48,"value":156}," + ",{"type":43,"tag":128,"props":158,"children":160},{"className":159},[],[161],{"type":48,"value":162},"invalidateQueries",{"type":48,"value":143},{"type":43,"tag":92,"props":165,"children":166},{},[167,172,183],{"type":43,"tag":117,"props":168,"children":169},{},[170],{"type":48,"value":171},"Query API",{"type":43,"tag":117,"props":173,"children":174},{},[175,181],{"type":43,"tag":128,"props":176,"children":178},{"className":177},[],[179],{"type":48,"value":180},"createApi",{"type":48,"value":182}," + generated hooks",{"type":43,"tag":117,"props":184,"children":185},{},[186,192,193,199],{"type":43,"tag":128,"props":187,"children":189},{"className":188},[],[190],{"type":48,"value":191},"useQuery",{"type":48,"value":135},{"type":43,"tag":128,"props":194,"children":196},{"className":195},[],[197],{"type":48,"value":198},"useMutation",{"type":48,"value":200}," per call site",{"type":43,"tag":92,"props":202,"children":203},{},[204,209,214],{"type":43,"tag":117,"props":205,"children":206},{},[207],{"type":48,"value":208},"Codegen",{"type":43,"tag":117,"props":210,"children":211},{},[212],{"type":48,"value":213},"OpenAPI and GraphQL generators ship with Redux Toolkit",{"type":43,"tag":117,"props":215,"children":216},{},[217],{"type":48,"value":218},"No first-party codegen; community plugins exist",{"type":43,"tag":92,"props":220,"children":221},{},[222,227,232],{"type":43,"tag":117,"props":223,"children":224},{},[225],{"type":48,"value":226},"Redux integration",{"type":43,"tag":117,"props":228,"children":229},{},[230],{"type":48,"value":231},"Native. Uses the same store, middleware, and DevTools",{"type":43,"tag":117,"props":233,"children":234},{},[235],{"type":48,"value":236},"None by default; can be used alongside Redux",{"type":43,"tag":92,"props":238,"children":239},{},[240,245,250],{"type":43,"tag":117,"props":241,"children":242},{},[243],{"type":48,"value":244},"Framework support",{"type":43,"tag":117,"props":246,"children":247},{},[248],{"type":48,"value":249},"React only",{"type":43,"tag":117,"props":251,"children":252},{},[253,255,264],{"type":48,"value":254},"React, Vue, Solid, Svelte, Angular, Lit, Preact (",{"type":43,"tag":256,"props":257,"children":261},"a",{"href":258,"rel":259},"https://tanstack.com/query/latest",[260],"nofollow",[262],{"type":48,"value":263},"source",{"type":48,"value":143},{"type":43,"tag":92,"props":266,"children":267},{},[268,273,286],{"type":43,"tag":117,"props":269,"children":270},{},[271],{"type":48,"value":272},"Gzipped bundle",{"type":43,"tag":117,"props":274,"children":275},{},[276,278,285],{"type":48,"value":277},"~13.6 KB (",{"type":43,"tag":256,"props":279,"children":282},{"href":280,"rel":281},"https://bundlephobia.com/package/@reduxjs/toolkit",[260],[283],{"type":48,"value":284},"Redux Toolkit 2.12.0 on Bundlephobia",{"type":48,"value":143},{"type":43,"tag":117,"props":287,"children":288},{},[289,290,297],{"type":48,"value":277},{"type":43,"tag":256,"props":291,"children":294},{"href":292,"rel":293},"https://bundlephobia.com/package/@tanstack/react-query",[260],[295],{"type":48,"value":296},"@tanstack/react-query 5.100.11 on Bundlephobia",{"type":48,"value":143},{"type":43,"tag":92,"props":299,"children":300},{},[301,306,311],{"type":43,"tag":117,"props":302,"children":303},{},[304],{"type":48,"value":305},"GitHub stars (May 2026)",{"type":43,"tag":117,"props":307,"children":308},{},[309],{"type":48,"value":310},"11.2k on the redux-toolkit repo",{"type":43,"tag":117,"props":312,"children":313},{},[314],{"type":48,"value":315},"49.5k on the TanStack/query repo",{"type":43,"tag":92,"props":317,"children":318},{},[319,324,329],{"type":43,"tag":117,"props":320,"children":321},{},[322],{"type":48,"value":323},"TypeScript story",{"type":43,"tag":117,"props":325,"children":326},{},[327],{"type":48,"value":328},"Good. Endpoint generics flow through to hooks",{"type":43,"tag":117,"props":330,"children":331},{},[332],{"type":48,"value":333},"Good. Query and mutation generics inferred from queryFn",{"type":43,"tag":92,"props":335,"children":336},{},[337,342,347],{"type":43,"tag":117,"props":338,"children":339},{},[340],{"type":48,"value":341},"DevTools",{"type":43,"tag":117,"props":343,"children":344},{},[345],{"type":48,"value":346},"Redux DevTools (shows every request as a Redux action)",{"type":43,"tag":117,"props":348,"children":349},{},[350],{"type":48,"value":351},"Dedicated React Query DevTools panel",{"type":43,"tag":92,"props":353,"children":354},{},[355,360,365],{"type":43,"tag":117,"props":356,"children":357},{},[358],{"type":48,"value":359},"Learning curve",{"type":43,"tag":117,"props":361,"children":362},{},[363],{"type":48,"value":364},"Steeper if you don't already know Redux",{"type":43,"tag":117,"props":366,"children":367},{},[368],{"type":48,"value":369},"Lower entry barrier; concepts are local to data fetching",{"type":43,"tag":44,"props":371,"children":372},{},[373],{"type":48,"value":374},"The two libraries cluster closely on bundle size and TypeScript quality, then diverge sharply on framework support and Redux integration. That's where the real decision lives.",{"type":43,"tag":66,"props":376,"children":378},{"id":377},"a-fair-head-to-head-list-create-mutation",[379],{"type":48,"value":380},"A fair head-to-head: list + create mutation",{"type":43,"tag":44,"props":382,"children":383},{},[384],{"type":48,"value":385},"To make the trade-offs concrete, here is the same use case in both libraries. We fetch a list of users, then create a new user with a mutation that invalidates the list so the UI updates automatically.",{"type":43,"tag":387,"props":388,"children":390},"h3",{"id":389},"the-rtk-query-version",[391],{"type":48,"value":392},"The RTK Query version",{"type":43,"tag":44,"props":394,"children":395},{},[396,398,403],{"type":48,"value":397},"We start with a ",{"type":43,"tag":51,"props":399,"children":400},{},[401],{"type":48,"value":402},"base API slice",{"type":48,"value":404}," that holds shared configuration. Every feature in the app injects its endpoints into this base.",{"type":43,"tag":44,"props":406,"children":407},{},[408],{"type":43,"tag":409,"props":410,"children":411},"strong",{},[412],{"type":48,"value":413},"src/api/base.api.ts",{"type":43,"tag":415,"props":416,"children":421},"pre",{"className":417,"code":419,"language":420,"meta":7},[418],"language-typescript","import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'\n\nexport const baseApi = createApi({\n  reducerPath: 'api',\n  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),\n  tagTypes: ['Users'],\n  endpoints: () => ({}),\n})\n","typescript",[422],{"type":43,"tag":128,"props":423,"children":424},{"__ignoreMap":7},[425],{"type":48,"value":419},{"type":43,"tag":44,"props":427,"children":428},{},[429,431,437,439,445],{"type":48,"value":430},"Notice the empty ",{"type":43,"tag":128,"props":432,"children":434},{"className":433},[],[435],{"type":48,"value":436},"endpoints",{"type":48,"value":438}," object. We leave it lean so each feature can add its own endpoints from its own file. The ",{"type":43,"tag":128,"props":440,"children":442},{"className":441},[],[443],{"type":48,"value":444},"tagTypes",{"type":48,"value":446}," are declared centrally because RTK Query needs to know about every tag upfront for cache invalidation to wire up correctly.",{"type":43,"tag":44,"props":448,"children":449},{},[450],{"type":48,"value":451},"Now to add the users feature:",{"type":43,"tag":44,"props":453,"children":454},{},[455],{"type":43,"tag":409,"props":456,"children":457},{},[458],{"type":48,"value":459},"src/features/users/users.api.ts",{"type":43,"tag":415,"props":461,"children":464},{"className":462,"code":463,"language":420,"meta":7},[418],"import { baseApi } from '@/api/base.api'\n\nexport type User = { id: string; name: string; email: string }\n\nconst usersApi = baseApi.injectEndpoints({\n  endpoints: builder => ({\n    getUsers: builder.query\u003CUser[], void>({\n      query: () => '/users',\n      providesTags: result =>\n        result\n          ? [\n              ...result.map(({ id }) => ({ type: 'Users' as const, id })),\n              { type: 'Users' as const, id: 'LIST' },\n            ]\n          : [{ type: 'Users' as const, id: 'LIST' }],\n    }),\n    createUser: builder.mutation\u003CUser, Omit\u003CUser, 'id'>>({\n      query: body => ({ url: '/users', method: 'POST', body }),\n      invalidatesTags: [{ type: 'Users', id: 'LIST' }],\n    }),\n  }),\n})\n\nexport const { useGetUsersQuery, useCreateUserMutation } = usersApi\n",[465],{"type":43,"tag":128,"props":466,"children":467},{"__ignoreMap":7},[468],{"type":48,"value":463},{"type":43,"tag":44,"props":470,"children":471},{},[472],{"type":48,"value":473},"And the component that consumes both hooks:",{"type":43,"tag":44,"props":475,"children":476},{},[477],{"type":43,"tag":409,"props":478,"children":479},{},[480],{"type":48,"value":481},"src/features/users/UsersList.tsx",{"type":43,"tag":415,"props":483,"children":488},{"className":484,"code":486,"language":487,"meta":7},[485],"language-tsx","import { useGetUsersQuery, useCreateUserMutation } from './users.api'\n\nconst UsersList = () => {\n  const { data: users, isLoading, error } = useGetUsersQuery()\n  const [createUser, { isLoading: isCreating }] = useCreateUserMutation()\n\n  if (isLoading) return \u003Cp>Loading...\u003C/p>\n  if (error) return \u003Cp>Could not load users.\u003C/p>\n\n  return (\n    \u003Cdiv>\n      \u003Cul>\n        {users?.map(user => (\n          \u003Cli key={user.id}>{user.name}\u003C/li>\n        ))}\n      \u003C/ul>\n      \u003Cbutton\n        onClick={() => createUser({ name: 'Ada', email: 'ada@example.com' })}\n        disabled={isCreating}\n      >\n        Add user\n      \u003C/button>\n    \u003C/div>\n  )\n}\n","tsx",[489],{"type":43,"tag":128,"props":490,"children":491},{"__ignoreMap":7},[492],{"type":48,"value":486},{"type":43,"tag":44,"props":494,"children":495},{},[496,498,503,505,511,513,518,519,525,527,533],{"type":48,"value":497},"The component dispatches no actions. The generated hooks handle the fetch, the cache, and the loading state. The refetch after the mutation is wired automatically by the ",{"type":43,"tag":128,"props":499,"children":501},{"className":500},[],[502],{"type":48,"value":141},{"type":48,"value":504}," on ",{"type":43,"tag":128,"props":506,"children":508},{"className":507},[],[509],{"type":48,"value":510},"createUser",{"type":48,"value":512}," and the ",{"type":43,"tag":128,"props":514,"children":516},{"className":515},[],[517],{"type":48,"value":133},{"type":48,"value":504},{"type":43,"tag":128,"props":520,"children":522},{"className":521},[],[523],{"type":48,"value":524},"getUsers",{"type":48,"value":526},". When the button is clicked, the mutation runs, the ",{"type":43,"tag":128,"props":528,"children":530},{"className":529},[],[531],{"type":48,"value":532},"Users / LIST",{"type":48,"value":534}," tag invalidates, and the list query refetches without a single line of orchestration in the component.",{"type":43,"tag":387,"props":536,"children":538},{"id":537},"the-tanstack-query-version",[539],{"type":48,"value":540},"The TanStack Query version",{"type":43,"tag":44,"props":542,"children":543},{},[544,546,551,553,558],{"type":48,"value":545},"TanStack Query doesn't ask you to declare endpoints centrally. You write the fetch function where you need it, then call ",{"type":43,"tag":128,"props":547,"children":549},{"className":548},[],[550],{"type":48,"value":191},{"type":48,"value":552}," or ",{"type":43,"tag":128,"props":554,"children":556},{"className":555},[],[557],{"type":48,"value":198},{"type":48,"value":559}," from a component or a hook.",{"type":43,"tag":44,"props":561,"children":562},{},[563],{"type":43,"tag":409,"props":564,"children":565},{},[566],{"type":48,"value":459},{"type":43,"tag":415,"props":568,"children":571},{"className":569,"code":570,"language":420,"meta":7},[418],"export type User = { id: string; name: string; email: string }\n\nexport const fetchUsers = async (): Promise\u003CUser[]> => {\n  const res = await fetch('/api/users')\n  if (!res.ok) throw new Error('Failed to fetch users')\n  return res.json()\n}\n\nexport const createUser = async (\n  body: Omit\u003CUser, 'id'>,\n): Promise\u003CUser> => {\n  const res = await fetch('/api/users', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify(body),\n  })\n  if (!res.ok) throw new Error('Failed to create user')\n  return res.json()\n}\n",[572],{"type":43,"tag":128,"props":573,"children":574},{"__ignoreMap":7},[575],{"type":48,"value":570},{"type":43,"tag":44,"props":577,"children":578},{},[579],{"type":48,"value":580},"And the component:",{"type":43,"tag":44,"props":582,"children":583},{},[584],{"type":43,"tag":409,"props":585,"children":586},{},[587],{"type":48,"value":481},{"type":43,"tag":415,"props":589,"children":592},{"className":590,"code":591,"language":487,"meta":7},[485],"import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'\nimport { fetchUsers, createUser, User } from './users.api'\n\nconst UsersList = () => {\n  const queryClient = useQueryClient()\n\n  const { data: users, isLoading, error } = useQuery({\n    queryKey: ['users'],\n    queryFn: fetchUsers,\n  })\n\n  const { mutate, isPending } = useMutation({\n    mutationFn: createUser,\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: ['users'] })\n    },\n  })\n\n  if (isLoading) return \u003Cp>Loading...\u003C/p>\n  if (error) return \u003Cp>Could not load users.\u003C/p>\n\n  return (\n    \u003Cdiv>\n      \u003Cul>\n        {users?.map(user => (\n          \u003Cli key={user.id}>{user.name}\u003C/li>\n        ))}\n      \u003C/ul>\n      \u003Cbutton\n        onClick={() => mutate({ name: 'Ada', email: 'ada@example.com' })}\n        disabled={isPending}\n      >\n        Add user\n      \u003C/button>\n    \u003C/div>\n  )\n}\n",[593],{"type":43,"tag":128,"props":594,"children":595},{"__ignoreMap":7},[596],{"type":48,"value":591},{"type":43,"tag":44,"props":598,"children":599},{},[600,602,607],{"type":48,"value":601},"Two things stand out side by side. The TanStack Query version has less ceremony. No base slice, no endpoint registration, no ",{"type":43,"tag":128,"props":603,"children":605},{"className":604},[],[606],{"type":48,"value":444},{"type":48,"value":608}," declaration. The trade-off is that cache invalidation is explicit. You name the query key, and on mutation success you tell the query client which key to invalidate. RTK Query trades a little upfront setup for the invalidation being declarative on the endpoint itself.",{"type":43,"tag":44,"props":610,"children":611},{},[612],{"type":48,"value":613},"Neither version is more correct than the other. They surface different priorities.",{"type":43,"tag":66,"props":615,"children":617},{"id":616},"when-to-pick-each",[618],{"type":48,"value":619},"When to pick each",{"type":43,"tag":44,"props":621,"children":622},{},[623],{"type":48,"value":624},"The choice tracks fairly cleanly to a few real-world scenarios.",{"type":43,"tag":387,"props":626,"children":628},{"id":627},"pick-rtk-query-when",[629],{"type":48,"value":630},"Pick RTK Query when",{"type":43,"tag":44,"props":632,"children":633},{},[634,639],{"type":43,"tag":409,"props":635,"children":636},{},[637],{"type":48,"value":638},"You're already using Redux Toolkit.",{"type":48,"value":640}," If your store, slices, and middleware are set up, RTK Query is the path of least resistance. It uses the same store, the same middleware pipeline, and the same DevTools. You don't add a second cache or a second mental model.",{"type":43,"tag":44,"props":642,"children":643},{},[644,649,651,657,659,665],{"type":43,"tag":409,"props":645,"children":646},{},[647],{"type":48,"value":648},"Your team values declarative cache invalidation.",{"type":48,"value":650}," Tag-based invalidation puts the relationship between queries and mutations on the endpoint definition itself, not on the component that triggers the mutation. A new developer reading the slice can see which queries refetch when ",{"type":43,"tag":128,"props":652,"children":654},{"className":653},[],[655],{"type":48,"value":656},"updateUser",{"type":48,"value":658}," runs. In TanStack Query, that linkage lives in the ",{"type":43,"tag":128,"props":660,"children":662},{"className":661},[],[663],{"type":48,"value":664},"onSuccess",{"type":48,"value":666}," of every mutation that touches it.",{"type":43,"tag":44,"props":668,"children":669},{},[670,675],{"type":43,"tag":409,"props":671,"children":672},{},[673],{"type":48,"value":674},"You need OpenAPI or GraphQL codegen without extra setup.",{"type":48,"value":676}," Redux Toolkit ships official generators for both. They produce a slice with typed endpoints from an OpenAPI spec or a GraphQL schema, which means the entire API surface is typed and refetched automatically when the spec changes.",{"type":43,"tag":387,"props":678,"children":680},{"id":679},"pick-tanstack-query-when",[681],{"type":48,"value":682},"Pick TanStack Query when",{"type":43,"tag":44,"props":684,"children":685},{},[686,691],{"type":43,"tag":409,"props":687,"children":688},{},[689],{"type":48,"value":690},"You're not already on Redux.",{"type":48,"value":692}," Pulling Redux Toolkit in only to use RTK Query means adopting a state-management library, a middleware chain, and a set of conventions you may not otherwise need. TanStack Query has no opinions about your client state. It manages server state in its own cache and leaves everything else alone.",{"type":43,"tag":44,"props":694,"children":695},{},[696,701,703,708],{"type":43,"tag":409,"props":697,"children":698},{},[699],{"type":48,"value":700},"You work across multiple frameworks.",{"type":48,"value":702}," TanStack Query officially supports React, Vue, Solid, Svelte, Angular, Lit, and Preact (",{"type":43,"tag":256,"props":704,"children":706},{"href":258,"rel":705},[260],[707],{"type":48,"value":263},{"type":48,"value":709},"). If your company runs a React product alongside a Vue admin panel, sharing the data layer means standardising on TanStack Query. RTK Query is React-only.",{"type":43,"tag":44,"props":711,"children":712},{},[713,718,720,726,728,734,735,741,743,749,751,757],{"type":43,"tag":409,"props":714,"children":715},{},[716],{"type":48,"value":717},"You want fine-grained control over query keys.",{"type":48,"value":719}," Query keys in TanStack Query are arrays, and you can structure them however you like: ",{"type":43,"tag":128,"props":721,"children":723},{"className":722},[],[724],{"type":48,"value":725},"['users']",{"type":48,"value":727},", ",{"type":43,"tag":128,"props":729,"children":731},{"className":730},[],[732],{"type":48,"value":733},"['users', userId]",{"type":48,"value":727},{"type":43,"tag":128,"props":736,"children":738},{"className":737},[],[739],{"type":48,"value":740},"['users', { status: 'active', page: 2 }]",{"type":48,"value":742},". Partial-match invalidation falls out of that structure naturally. You call ",{"type":43,"tag":128,"props":744,"children":746},{"className":745},[],[747],{"type":48,"value":748},"invalidateQueries({ queryKey: ['users'] })",{"type":48,"value":750}," and every key starting with ",{"type":43,"tag":128,"props":752,"children":754},{"className":753},[],[755],{"type":48,"value":756},"users",{"type":48,"value":758}," refetches.",{"type":43,"tag":387,"props":760,"children":762},{"id":761},"pick-either-when-it-doesnt-really-matter",[763],{"type":48,"value":764},"Pick either when it doesn't really matter",{"type":43,"tag":44,"props":766,"children":767},{},[768],{"type":48,"value":769},"For a small app with one or two API resources and no Redux footprint, both libraries do the job, and the deciding factor is usually familiarity. I would not waste a week comparing the two when the app has six endpoints. Pick whichever your team has used before and move on.",{"type":43,"tag":66,"props":771,"children":773},{"id":772},"caching-model-where-the-real-differences-live",[774],{"type":48,"value":775},"Caching model: where the real differences live",{"type":43,"tag":44,"props":777,"children":778},{},[779],{"type":48,"value":780},"The surface APIs look similar. The caching internals differ in ways that affect how you reason about invalidation as the app grows.",{"type":43,"tag":387,"props":782,"children":784},{"id":783},"how-rtk-query-caches-and-invalidates",[785],{"type":48,"value":786},"How RTK Query caches and invalidates",{"type":43,"tag":44,"props":788,"children":789},{},[790,792,798,800,806,808,814],{"type":48,"value":791},"RTK Query stores every query result keyed by the endpoint name and the serialised query argument. A query for ",{"type":43,"tag":128,"props":793,"children":795},{"className":794},[],[796],{"type":48,"value":797},"getUserById(42)",{"type":48,"value":799}," lives at a different cache entry to ",{"type":43,"tag":128,"props":801,"children":803},{"className":802},[],[804],{"type":48,"value":805},"getUserById(43)",{"type":48,"value":807},". The library deduplicates concurrent requests. If two components call ",{"type":43,"tag":128,"props":809,"children":811},{"className":810},[],[812],{"type":48,"value":813},"useGetUserByIdQuery(42)",{"type":48,"value":815}," in the same render, only one network request goes out, and both components subscribe to the same cached result.",{"type":43,"tag":44,"props":817,"children":818},{},[819,821,826,828,833,835,840],{"type":48,"value":820},"Invalidation runs through ",{"type":43,"tag":51,"props":822,"children":823},{},[824],{"type":48,"value":825},"tags",{"type":48,"value":827},". A query says \"I provide this data\" via ",{"type":43,"tag":128,"props":829,"children":831},{"className":830},[],[832],{"type":48,"value":133},{"type":48,"value":834},". A mutation says \"I make this data stale\" via ",{"type":43,"tag":128,"props":836,"children":838},{"className":837},[],[839],{"type":48,"value":141},{"type":48,"value":841},". When the tags overlap, the affected query refetches automatically.",{"type":43,"tag":415,"props":843,"children":846},{"className":844,"code":845,"language":420,"meta":7},[418],"getUsers: builder.query\u003CUser[], void>({\n  query: () => '/users',\n  providesTags: result =>\n    result\n      ? [\n          ...result.map(({ id }) => ({ type: 'Users' as const, id })),\n          { type: 'Users' as const, id: 'LIST' },\n        ]\n      : [{ type: 'Users' as const, id: 'LIST' }],\n}),\nupdateUser: builder.mutation\u003CUser, { id: string; name: string }>({\n  query: ({ id, ...patch }) => ({\n    url: `/users/${id}`,\n    method: 'PATCH',\n    body: patch,\n  }),\n  invalidatesTags: (result, error, { id }) => [{ type: 'Users', id }],\n}),\n",[847],{"type":43,"tag":128,"props":848,"children":849},{"__ignoreMap":7},[850],{"type":48,"value":845},{"type":43,"tag":44,"props":852,"children":853},{},[854,856,861,863,869],{"type":48,"value":855},"When ",{"type":43,"tag":128,"props":857,"children":859},{"className":858},[],[860],{"type":48,"value":656},{"type":48,"value":862}," fires, only the cache entry for that specific user invalidates. The list query, which provides a ",{"type":43,"tag":128,"props":864,"children":866},{"className":865},[],[867],{"type":48,"value":868},"LIST",{"type":48,"value":870}," tag plus per-ID tags, refetches the affected entry but doesn't necessarily refetch the whole list. That precision matters when a list view shares the network with individual detail views and you don't want every edit to trigger a full list refetch.",{"type":43,"tag":44,"props":872,"children":873},{},[874,876,881],{"type":48,"value":875},"The trade-off is that you have to register tag types upfront in ",{"type":43,"tag":128,"props":877,"children":879},{"className":878},[],[880],{"type":48,"value":180},{"type":48,"value":882},", and every mutation has to declare what it invalidates. The verbosity is real. The pay-off is that cache relationships live in the slice file. No hunting through component code to find out what refetches when.",{"type":43,"tag":387,"props":884,"children":886},{"id":885},"how-tanstack-query-caches-and-invalidates",[887],{"type":48,"value":888},"How TanStack Query caches and invalidates",{"type":43,"tag":44,"props":890,"children":891},{},[892,894,899,901,906,908,914],{"type":48,"value":893},"TanStack Query stores results keyed by the ",{"type":43,"tag":51,"props":895,"children":896},{},[897],{"type":48,"value":898},"query key",{"type":48,"value":900},", an array you provide on every ",{"type":43,"tag":128,"props":902,"children":904},{"className":903},[],[905],{"type":48,"value":191},{"type":48,"value":907}," call. The key is hashed deterministically, so two calls with ",{"type":43,"tag":128,"props":909,"children":911},{"className":910},[],[912],{"type":48,"value":913},"['users', { status: 'active' }]",{"type":48,"value":915}," resolve to the same cache entry. Components that share a key share a cached result, and the library deduplicates network requests the same way RTK Query does.",{"type":43,"tag":44,"props":917,"children":918},{},[919,921,927,929,934],{"type":48,"value":920},"Invalidation is imperative. You call ",{"type":43,"tag":128,"props":922,"children":924},{"className":923},[],[925],{"type":48,"value":926},"queryClient.invalidateQueries({ queryKey: ['users'] })",{"type":48,"value":928},", and every query whose key starts with ",{"type":43,"tag":128,"props":930,"children":932},{"className":931},[],[933],{"type":48,"value":725},{"type":48,"value":935}," is marked stale and refetched on next use.",{"type":43,"tag":415,"props":937,"children":940},{"className":938,"code":939,"language":487,"meta":7},[485],"const { mutate } = useMutation({\n  mutationFn: updateUser,\n  onSuccess: (_, { id }) => {\n    queryClient.invalidateQueries({ queryKey: ['users', id] })\n    queryClient.invalidateQueries({ queryKey: ['users', 'list'] })\n  },\n})\n",[941],{"type":43,"tag":128,"props":942,"children":943},{"__ignoreMap":7},[944],{"type":48,"value":939},{"type":43,"tag":44,"props":946,"children":947},{},[948,950,955,957,963],{"type":48,"value":949},"The benefit is that prefix matching gives you flexible invalidation patterns for free. Want to refetch everything users-related? ",{"type":43,"tag":128,"props":951,"children":953},{"className":952},[],[954],{"type":48,"value":748},{"type":48,"value":956}," does it. Only a specific user? ",{"type":43,"tag":128,"props":958,"children":960},{"className":959},[],[961],{"type":48,"value":962},"invalidateQueries({ queryKey: ['users', 42] })",{"type":48,"value":964},". Every list view across the app? Use a key prefix that matches them. The library doesn't constrain how you structure keys.",{"type":43,"tag":44,"props":966,"children":967},{},[968,970,976,978,984,986,991],{"type":48,"value":969},"The downside, in my experience, is discipline. Without a convention for how keys are structured, you can end up with ",{"type":43,"tag":128,"props":971,"children":973},{"className":972},[],[974],{"type":48,"value":975},"['user', id]",{"type":48,"value":977}," in one place and ",{"type":43,"tag":128,"props":979,"children":981},{"className":980},[],[982],{"type":48,"value":983},"['users', id]",{"type":48,"value":985}," in another, and the prefix-match invalidation silently fails. Teams that adopt TanStack Query at scale almost always end up writing a ",{"type":43,"tag":51,"props":987,"children":988},{},[989],{"type":48,"value":990},"query key factory",{"type":48,"value":992},", a helper that builds keys consistently across the codebase.",{"type":43,"tag":387,"props":994,"children":996},{"id":995},"stale-while-revalidate-and-refetch-behaviour",[997],{"type":48,"value":998},"Stale-while-revalidate and refetch behaviour",{"type":43,"tag":44,"props":1000,"children":1001},{},[1002],{"type":48,"value":1003},"Both libraries follow stale-while-revalidate semantics by default. They serve the cached value immediately, then refetch in the background if the data is considered stale. The defaults differ.",{"type":43,"tag":44,"props":1005,"children":1006},{},[1007,1009,1015,1017,1023,1025,1031,1033,1039],{"type":48,"value":1008},"TanStack Query treats data as stale immediately (",{"type":43,"tag":128,"props":1010,"children":1012},{"className":1011},[],[1013],{"type":48,"value":1014},"staleTime: 0",{"type":48,"value":1016}," by default), which means it refetches on every component mount, window focus, and reconnect. That's aggressive, and for most apps it's the right default, because users expect data to be fresh when they come back to the tab. You can tune it per query with ",{"type":43,"tag":128,"props":1018,"children":1020},{"className":1019},[],[1021],{"type":48,"value":1022},"staleTime",{"type":48,"value":1024}," and ",{"type":43,"tag":128,"props":1026,"children":1028},{"className":1027},[],[1029],{"type":48,"value":1030},"gcTime",{"type":48,"value":1032}," (formerly ",{"type":43,"tag":128,"props":1034,"children":1036},{"className":1035},[],[1037],{"type":48,"value":1038},"cacheTime",{"type":48,"value":1040},").",{"type":43,"tag":44,"props":1042,"children":1043},{},[1044,1046,1052,1054,1060,1061,1067],{"type":48,"value":1045},"RTK Query treats data as fresh forever by default. Queries don't refetch on mount unless you set ",{"type":43,"tag":128,"props":1047,"children":1049},{"className":1048},[],[1050],{"type":48,"value":1051},"refetchOnMountOrArgChange",{"type":48,"value":1053}," on the endpoint or the hook call. Background refetching is opt-in via ",{"type":43,"tag":128,"props":1055,"children":1057},{"className":1056},[],[1058],{"type":48,"value":1059},"refetchOnFocus",{"type":48,"value":1024},{"type":43,"tag":128,"props":1062,"children":1064},{"className":1063},[],[1065],{"type":48,"value":1066},"refetchOnReconnect",{"type":48,"value":1068},", which are off by default. The conservative defaults mean fewer surprise network calls, but you'll set these flags on most apps that need TanStack-Query-style freshness.",{"type":43,"tag":44,"props":1070,"children":1071},{},[1072],{"type":48,"value":1073},"The two defaults frame the trade-off differently. TanStack Query optimises for \"the user always sees fresh data, even if that costs a request.\" RTK Query optimises for \"no request runs unless I asked for it.\"",{"type":43,"tag":66,"props":1075,"children":1077},{"id":1076},"migration-considerations",[1078],{"type":48,"value":1079},"Migration considerations",{"type":43,"tag":44,"props":1081,"children":1082},{},[1083],{"type":48,"value":1084},"Teams thinking about moving from one library to the other usually have one of two motivations. They're already on Redux Toolkit and want the smaller mental model TanStack Query offers, or they're already on TanStack Query and want RTK Query's tag-based invalidation and codegen.",{"type":43,"tag":44,"props":1086,"children":1087},{},[1088],{"type":48,"value":1089},"Both directions are feasible. Both are work.",{"type":43,"tag":44,"props":1091,"children":1092},{},[1093],{"type":48,"value":1094},"The good news is that neither library is all-or-nothing. RTK Query endpoints are scoped per slice. TanStack Query hooks are scoped per component. You can migrate one feature at a time, leave the rest alone, and ship the partial migration without breaking anything. I've done exactly that on apps where one section of the product moved to TanStack Query while the rest stayed on RTK Query for months. The two caches coexist fine because they don't share state.",{"type":43,"tag":44,"props":1096,"children":1097},{},[1098,1100,1106,1108,1114,1116,1121,1123,1128,1130,1135],{"type":48,"value":1099},"The reshaping work is mostly in cache invalidation. RTK Query's tag system maps onto TanStack Query query keys, but not cleanly. A tag like ",{"type":43,"tag":128,"props":1101,"children":1103},{"className":1102},[],[1104],{"type":48,"value":1105},"{ type: 'Users', id: 'LIST' }",{"type":48,"value":1107}," becomes a query key like ",{"type":43,"tag":128,"props":1109,"children":1111},{"className":1110},[],[1112],{"type":48,"value":1113},"['users', 'list']",{"type":48,"value":1115},". The relationship that was declarative on the endpoint becomes imperative on the mutation. You'll touch every mutation's ",{"type":43,"tag":128,"props":1117,"children":1119},{"className":1118},[],[1120],{"type":48,"value":664},{"type":48,"value":1122}," to call ",{"type":43,"tag":128,"props":1124,"children":1126},{"className":1125},[],[1127],{"type":48,"value":162},{"type":48,"value":1129}," instead of relying on ",{"type":43,"tag":128,"props":1131,"children":1133},{"className":1132},[],[1134],{"type":48,"value":141},{"type":48,"value":1136},". Plan for it.",{"type":43,"tag":44,"props":1138,"children":1139},{},[1140,1142,1148,1150,1156],{"type":48,"value":1141},"Error shapes also change. RTK Query's ",{"type":43,"tag":128,"props":1143,"children":1145},{"className":1144},[],[1146],{"type":48,"value":1147},"FetchBaseQueryError",{"type":48,"value":1149}," does not have a one-to-one TanStack Query equivalent. TanStack Query takes whatever your ",{"type":43,"tag":128,"props":1151,"children":1153},{"className":1152},[],[1154],{"type":48,"value":1155},"queryFn",{"type":48,"value":1157}," throws, which means you'll standardise on a single error type in your fetch wrappers as part of the migration.",{"type":43,"tag":44,"props":1159,"children":1160},{},[1161,1163,1169,1171,1177,1179,1185,1187,1193,1194,1200,1202,1208,1210,1216],{"type":48,"value":1162},"Optimistic updates work in both libraries, but the APIs differ. RTK Query uses ",{"type":43,"tag":128,"props":1164,"children":1166},{"className":1165},[],[1167],{"type":48,"value":1168},"onQueryStarted",{"type":48,"value":1170}," with ",{"type":43,"tag":128,"props":1172,"children":1174},{"className":1173},[],[1175],{"type":48,"value":1176},"updateQueryData",{"type":48,"value":1178}," and a ",{"type":43,"tag":128,"props":1180,"children":1182},{"className":1181},[],[1183],{"type":48,"value":1184},"patchResult.undo()",{"type":48,"value":1186}," rollback. TanStack Query uses ",{"type":43,"tag":128,"props":1188,"children":1190},{"className":1189},[],[1191],{"type":48,"value":1192},"onMutate",{"type":48,"value":727},{"type":43,"tag":128,"props":1195,"children":1197},{"className":1196},[],[1198],{"type":48,"value":1199},"onError",{"type":48,"value":1201},", and ",{"type":43,"tag":128,"props":1203,"children":1205},{"className":1204},[],[1206],{"type":48,"value":1207},"onSettled",{"type":48,"value":1209}," with explicit cache snapshotting via ",{"type":43,"tag":128,"props":1211,"children":1213},{"className":1212},[],[1214],{"type":48,"value":1215},"queryClient.setQueryData",{"type":48,"value":1217},". Neither is harder than the other. They're different. If your app leans heavily on optimistic UI, expect to rewrite each one during the migration.",{"type":43,"tag":66,"props":1219,"children":1221},{"id":1220},"devtools-and-the-integration-story",[1222],{"type":48,"value":1223},"DevTools and the integration story",{"type":43,"tag":44,"props":1225,"children":1226},{},[1227],{"type":48,"value":1228},"Both libraries have dedicated DevTools, but the experience differs.",{"type":43,"tag":44,"props":1230,"children":1231},{},[1232],{"type":48,"value":1233},"RTK Query piggybacks on Redux DevTools. Every query and mutation appears as a Redux action in the action log. If you're already debugging via Redux DevTools, your data fetching is in the same pane as your reducers, selectors, and dispatched actions. The continuity is genuinely useful when tracing why a component re-rendered.",{"type":43,"tag":44,"props":1235,"children":1236},{},[1237,1239,1245],{"type":48,"value":1238},"TanStack Query ships its own panel. The ",{"type":43,"tag":128,"props":1240,"children":1242},{"className":1241},[],[1243],{"type":48,"value":1244},"\u003CReactQueryDevtools>",{"type":48,"value":1246}," component shows every query, its status (fresh, fetching, stale, inactive), and its data, and lets you trigger refetches and invalidations manually. It is focused exclusively on data fetching, with no Redux noise. For an app that doesn't use Redux at all, the dedicated panel is cleaner than wedging server-state debugging into a general-purpose state DevTools.",{"type":43,"tag":44,"props":1248,"children":1249},{},[1250],{"type":48,"value":1251},"Which one wins depends on what you already use. If Redux DevTools is open in your browser anyway, RTK Query has the better integration story. If you don't use Redux, the standalone TanStack Query DevTools is more focused and arguably easier to learn.",{"type":43,"tag":66,"props":1253,"children":1255},{"id":1254},"typescript-story",[1256],{"type":48,"value":323},{"type":43,"tag":44,"props":1258,"children":1259},{},[1260],{"type":48,"value":1261},"Both libraries do well in TypeScript, and the differences are mostly stylistic.",{"type":43,"tag":44,"props":1263,"children":1264},{},[1265,1267,1273,1275,1281,1283,1289],{"type":48,"value":1266},"RTK Query's hooks are generated from the endpoint definitions. When you write ",{"type":43,"tag":128,"props":1268,"children":1270},{"className":1269},[],[1271],{"type":48,"value":1272},"builder.query\u003CUser[], void>",{"type":48,"value":1274},", the generated ",{"type":43,"tag":128,"props":1276,"children":1278},{"className":1277},[],[1279],{"type":48,"value":1280},"useGetUsersQuery",{"type":48,"value":1282}," hook returns ",{"type":43,"tag":128,"props":1284,"children":1286},{"className":1285},[],[1287],{"type":48,"value":1288},"{ data: User[] | undefined, isLoading: boolean, error: FetchBaseQueryError | undefined, ... }",{"type":48,"value":1290}," automatically. You never write the hook's return type yourself. The endpoint generics flow through.",{"type":43,"tag":44,"props":1292,"children":1293},{},[1294,1296,1301,1303,1308,1310,1316,1318,1324,1326,1332,1334,1340],{"type":48,"value":1295},"TanStack Query's hooks infer their types from the ",{"type":43,"tag":128,"props":1297,"children":1299},{"className":1298},[],[1300],{"type":48,"value":1155},{"type":48,"value":1302}," return type. If ",{"type":43,"tag":128,"props":1304,"children":1306},{"className":1305},[],[1307],{"type":48,"value":1155},{"type":48,"value":1309}," returns ",{"type":43,"tag":128,"props":1311,"children":1313},{"className":1312},[],[1314],{"type":48,"value":1315},"Promise\u003CUser[]>",{"type":48,"value":1317},", then ",{"type":43,"tag":128,"props":1319,"children":1321},{"className":1320},[],[1322],{"type":48,"value":1323},"useQuery({ queryKey: ['users'], queryFn })",{"type":48,"value":1325}," gives you ",{"type":43,"tag":128,"props":1327,"children":1329},{"className":1328},[],[1330],{"type":48,"value":1331},"data: User[] | undefined",{"type":48,"value":1333},". You can also pass explicit generics, like ",{"type":43,"tag":128,"props":1335,"children":1337},{"className":1336},[],[1338],{"type":48,"value":1339},"useQuery\u003CUser[], Error>(...)",{"type":48,"value":1341},", when inference isn't enough. Either way the type story is clean.",{"type":43,"tag":44,"props":1343,"children":1344},{},[1345,1347,1353,1355,1360,1362,1368],{"type":48,"value":1346},"The one practical difference is that RTK Query's ",{"type":43,"tag":128,"props":1348,"children":1350},{"className":1349},[],[1351],{"type":48,"value":1352},"skipToken",{"type":48,"value":1354}," is type-safe in a way that's hard to replicate in TanStack Query. Passing ",{"type":43,"tag":128,"props":1356,"children":1358},{"className":1357},[],[1359],{"type":48,"value":1352},{"type":48,"value":1361}," to a hook tells the type system \"this query is disabled,\" and TypeScript enforces the condition at the call site. TanStack Query achieves the same outcome through the ",{"type":43,"tag":128,"props":1363,"children":1365},{"className":1364},[],[1366],{"type":48,"value":1367},"enabled",{"type":48,"value":1369}," option, but the type system doesn't enforce it the same way. You still pass the argument, and the library skips the fetch at runtime.",{"type":43,"tag":66,"props":1371,"children":1373},{"id":1372},"performance-and-bundle-size",[1374],{"type":48,"value":1375},"Performance and bundle size",{"type":43,"tag":44,"props":1377,"children":1378},{},[1379],{"type":48,"value":1380},"Bundle size, measured at the time of writing on Bundlephobia, is a wash:",{"type":43,"tag":1382,"props":1383,"children":1384},"ul",{},[1385,1410],{"type":43,"tag":1386,"props":1387,"children":1388},"li",{},[1389,1395,1397,1402,1404,1409],{"type":43,"tag":128,"props":1390,"children":1392},{"className":1391},[],[1393],{"type":48,"value":1394},"@reduxjs/toolkit",{"type":48,"value":1396}," v2.12.0: 37 KB minified, ",{"type":43,"tag":409,"props":1398,"children":1399},{},[1400],{"type":48,"value":1401},"13.6 KB gzipped",{"type":48,"value":1403}," (",{"type":43,"tag":256,"props":1405,"children":1407},{"href":280,"rel":1406},[260],[1408],{"type":48,"value":263},{"type":48,"value":143},{"type":43,"tag":1386,"props":1411,"children":1412},{},[1413,1419,1421,1425,1426,1431],{"type":43,"tag":128,"props":1414,"children":1416},{"className":1415},[],[1417],{"type":48,"value":1418},"@tanstack/react-query",{"type":48,"value":1420}," v5.100.11: 46.2 KB minified, ",{"type":43,"tag":409,"props":1422,"children":1423},{},[1424],{"type":48,"value":1401},{"type":48,"value":1403},{"type":43,"tag":256,"props":1427,"children":1429},{"href":292,"rel":1428},[260],[1430],{"type":48,"value":263},{"type":48,"value":143},{"type":43,"tag":44,"props":1433,"children":1434},{},[1435,1437,1442,1444,1450],{"type":48,"value":1436},"The numbers look identical for a reason. Both libraries are mature, tree-shakeable, and have been through several rounds of size optimisation. What matters is the ",{"type":43,"tag":51,"props":1438,"children":1439},{},[1440],{"type":48,"value":1441},"total",{"type":48,"value":1443}," footprint. If you adopt RTK Query, you also pull in the Redux store and middleware, which add to the bundle. If you adopt TanStack Query, you only pay for the query library and a ",{"type":43,"tag":128,"props":1445,"children":1447},{"className":1446},[],[1448],{"type":48,"value":1449},"QueryClientProvider",{"type":48,"value":1451}," at the root.",{"type":43,"tag":44,"props":1453,"children":1454},{},[1455],{"type":48,"value":1456},"For apps that already use Redux, the marginal cost of adding RTK Query is close to nothing. For apps that don't, TanStack Query is the lighter total footprint. The decision shouldn't hinge on bundle size. Both are well within acceptable budgets for any production app.",{"type":43,"tag":44,"props":1458,"children":1459},{},[1460],{"type":48,"value":1461},"Runtime performance is similarly close. Both libraries deduplicate concurrent requests for the same key. Both batch re-renders. Both let you opt into structural sharing so unchanged data doesn't trigger downstream renders. There is no real-world performance gap that would tip the decision either way.",{"type":43,"tag":66,"props":1463,"children":1465},{"id":1464},"ecosystem-and-tooling",[1466],{"type":48,"value":1467},"Ecosystem and tooling",{"type":43,"tag":44,"props":1469,"children":1470},{},[1471],{"type":48,"value":1472},"This is where the libraries diverge most visibly outside of the API design itself.",{"type":43,"tag":44,"props":1474,"children":1475},{},[1476,1481],{"type":43,"tag":409,"props":1477,"children":1478},{},[1479],{"type":48,"value":1480},"Codegen.",{"type":48,"value":1482}," RTK Query ships official OpenAPI and GraphQL code generators. Point the generator at a spec, and you get a fully-typed slice with every endpoint. For teams with a backend that publishes an OpenAPI document, that is a meaningful productivity multiplier. TanStack Query has no official codegen, though community plugins exist (orval, openapi-codegen) and produce hooks-shaped output. The difference is \"ships with the library\" versus \"you wire it up yourself\".",{"type":43,"tag":44,"props":1484,"children":1485},{},[1486,1491,1493,1499,1500,1506,1507,1513,1515,1521,1522,1528,1529,1535],{"type":43,"tag":409,"props":1487,"children":1488},{},[1489],{"type":48,"value":1490},"Server state libraries.",{"type":48,"value":1492}," TanStack Query has a wide set of adjacent libraries: ",{"type":43,"tag":128,"props":1494,"children":1496},{"className":1495},[],[1497],{"type":48,"value":1498},"@tanstack/react-router",{"type":48,"value":727},{"type":43,"tag":128,"props":1501,"children":1503},{"className":1502},[],[1504],{"type":48,"value":1505},"@tanstack/react-table",{"type":48,"value":727},{"type":43,"tag":128,"props":1508,"children":1510},{"className":1509},[],[1511],{"type":48,"value":1512},"@tanstack/react-virtual",{"type":48,"value":1514},", and so on. They aren't required to use TanStack Query, but they share design philosophy and integrate cleanly. RTK Query lives inside Redux Toolkit, so the ecosystem is the Redux ecosystem: ",{"type":43,"tag":128,"props":1516,"children":1518},{"className":1517},[],[1519],{"type":48,"value":1520},"reselect",{"type":48,"value":727},{"type":43,"tag":128,"props":1523,"children":1525},{"className":1524},[],[1526],{"type":48,"value":1527},"redux-saga",{"type":48,"value":727},{"type":43,"tag":128,"props":1530,"children":1532},{"className":1531},[],[1533],{"type":48,"value":1534},"redux-thunk",{"type":48,"value":1536},", and the various middleware libraries.",{"type":43,"tag":44,"props":1538,"children":1539},{},[1540,1545,1547,1553,1555,1561,1562,1568,1570,1576],{"type":43,"tag":409,"props":1541,"children":1542},{},[1543],{"type":48,"value":1544},"Persistence.",{"type":48,"value":1546}," TanStack Query has well-supported persistence plugins (",{"type":43,"tag":128,"props":1548,"children":1550},{"className":1549},[],[1551],{"type":48,"value":1552},"persistQueryClient",{"type":48,"value":1554},") for storing the cache in ",{"type":43,"tag":128,"props":1556,"children":1558},{"className":1557},[],[1559],{"type":48,"value":1560},"localStorage",{"type":48,"value":727},{"type":43,"tag":128,"props":1563,"children":1565},{"className":1564},[],[1566],{"type":48,"value":1567},"IndexedDB",{"type":48,"value":1569},", or a custom storage adapter. Useful for offline-first apps. RTK Query does not ship persistence by default. You'd use ",{"type":43,"tag":128,"props":1571,"children":1573},{"className":1572},[],[1574],{"type":48,"value":1575},"redux-persist",{"type":48,"value":1577}," with the slice, which works but takes more wiring.",{"type":43,"tag":44,"props":1579,"children":1580},{},[1581,1586,1588,1594,1595,1601],{"type":43,"tag":409,"props":1582,"children":1583},{},[1584],{"type":48,"value":1585},"Suspense and concurrent rendering.",{"type":48,"value":1587}," Both libraries support React's ",{"type":43,"tag":128,"props":1589,"children":1591},{"className":1590},[],[1592],{"type":48,"value":1593},"Suspense",{"type":48,"value":1024},{"type":43,"tag":128,"props":1596,"children":1598},{"className":1597},[],[1599],{"type":48,"value":1600},"useTransition",{"type":48,"value":1602}," hooks. TanStack Query's support is more mature and better documented at this point. RTK Query has caught up in recent versions, but the TanStack ecosystem has been on this longer.",{"type":43,"tag":66,"props":1604,"children":1606},{"id":1605},"adoption-signals",[1607],{"type":48,"value":1608},"Adoption signals",{"type":43,"tag":44,"props":1610,"children":1611},{},[1612],{"type":48,"value":1613},"A practical signal that's hard to fake is weekly download counts and active development.",{"type":43,"tag":1382,"props":1615,"children":1616},{},[1617,1637,1656],{"type":43,"tag":1386,"props":1618,"children":1619},{},[1620,1622,1627,1629,1636],{"type":48,"value":1621},"Redux Toolkit sits at around ",{"type":43,"tag":409,"props":1623,"children":1624},{},[1625],{"type":48,"value":1626},"20 million weekly downloads",{"type":48,"value":1628}," on npm, with the latest release being 2.12.0 (",{"type":43,"tag":256,"props":1630,"children":1633},{"href":1631,"rel":1632},"https://www.npmjs.com/package/@reduxjs/toolkit",[260],[1634],{"type":48,"value":1635},"npm",{"type":48,"value":1040},{"type":43,"tag":1386,"props":1638,"children":1639},{},[1640,1642,1647,1649,1655],{"type":48,"value":1641},"TanStack Query's React adapter alone sees roughly ",{"type":43,"tag":409,"props":1643,"children":1644},{},[1645],{"type":48,"value":1646},"56 million weekly downloads",{"type":48,"value":1648}," on npm (",{"type":43,"tag":256,"props":1650,"children":1653},{"href":1651,"rel":1652},"https://www.npmjs.com/package/@tanstack/react-query",[260],[1654],{"type":48,"value":1635},{"type":48,"value":1040},{"type":43,"tag":1386,"props":1657,"children":1658},{},[1659,1661,1666,1668,1673,1675,1682],{"type":48,"value":1660},"GitHub stars: TanStack/query is around ",{"type":43,"tag":409,"props":1662,"children":1663},{},[1664],{"type":48,"value":1665},"49.5k",{"type":48,"value":1667},", redux-toolkit is around ",{"type":43,"tag":409,"props":1669,"children":1670},{},[1671],{"type":48,"value":1672},"11.2k",{"type":48,"value":1674}," as of May 2026 (",{"type":43,"tag":256,"props":1676,"children":1679},{"href":1677,"rel":1678},"https://api.github.com/repos/TanStack/query",[260],[1680],{"type":48,"value":1681},"GitHub API",{"type":48,"value":1040},{"type":43,"tag":44,"props":1684,"children":1685},{},[1686],{"type":48,"value":1687},"TanStack Query has additional adapters (Vue, Svelte, Solid, Angular) with their own download counts on top of this React figure, so the cross-framework total is higher still. The gap reflects TanStack Query reaching further than the Redux ecosystem. Both are healthy projects with active maintenance.",{"type":43,"tag":66,"props":1689,"children":1691},{"id":1690},"so-which-one",[1692],{"type":48,"value":1693},"So which one?",{"type":43,"tag":44,"props":1695,"children":1696},{},[1697],{"type":48,"value":1698},"If you take nothing else from this article, take this. The choice between RTK Query and TanStack Query is rarely about the libraries themselves. It's about what's already in your stack and what your team is comfortable maintaining.",{"type":43,"tag":44,"props":1700,"children":1701},{},[1702],{"type":48,"value":1703},"I would recommend RTK Query for any project that already uses Redux Toolkit. The integration is genuinely free (same store, same middleware, same DevTools), and the tag-based invalidation is a pleasant pattern once you've internalised it. I would recommend TanStack Query for any project that doesn't have Redux, especially if there's any chance the data layer will need to be shared with a Vue, Svelte, or Angular surface down the line. The framework-agnostic story alone justifies the choice.",{"type":43,"tag":44,"props":1705,"children":1706},{},[1707],{"type":48,"value":1708},"What I would not recommend is mixing both in the same React application. You'll end up with two caches, two invalidation models, two sets of DevTools, and two ways for a junior developer on the team to fetch users. Pick one per app and stay consistent.",{"type":43,"tag":44,"props":1710,"children":1711},{},[1712,1714,1720],{"type":48,"value":1713},"If you want to see TanStack Query used outside of HTTP requests (Browser APIs, IndexedDB, Web Workers), that is covered in ",{"type":43,"tag":256,"props":1715,"children":1717},{"href":1716},"/blog/tanstack-query-is-not-just-for-api-requests",[1718],{"type":48,"value":1719},"TanStack Query is not just for API requests",{"type":48,"value":1721},".",{"title":7,"searchDepth":1723,"depth":1723,"links":1724},2,[1725,1726,1727,1732,1737,1742,1743,1744,1745,1746,1747,1748],{"id":68,"depth":1723,"text":71},{"id":79,"depth":1723,"text":82},{"id":377,"depth":1723,"text":380,"children":1728},[1729,1731],{"id":389,"depth":1730,"text":392},3,{"id":537,"depth":1730,"text":540},{"id":616,"depth":1723,"text":619,"children":1733},[1734,1735,1736],{"id":627,"depth":1730,"text":630},{"id":679,"depth":1730,"text":682},{"id":761,"depth":1730,"text":764},{"id":772,"depth":1723,"text":775,"children":1738},[1739,1740,1741],{"id":783,"depth":1730,"text":786},{"id":885,"depth":1730,"text":888},{"id":995,"depth":1730,"text":998},{"id":1076,"depth":1723,"text":1079},{"id":1220,"depth":1723,"text":1223},{"id":1254,"depth":1723,"text":323},{"id":1372,"depth":1723,"text":1375},{"id":1464,"depth":1723,"text":1467},{"id":1605,"depth":1723,"text":1608},{"id":1690,"depth":1723,"text":1693},"markdown","content:articles:react:rtk-query-vs-tanstack-query.md","content","articles/react/rtk-query-vs-tanstack-query.md","articles/react/rtk-query-vs-tanstack-query","md",1779373153743]