TanStack Query vs SWR: Choosing a Data-Fetching Library
TanStack Query and SWR both cache and revalidate server data in React, but differ in scope, mutation handling, and how much they manage for you.
TanStack Query (formerly React Query) and SWR are both libraries for fetching, caching, and synchronizing server data in React apps, and both are built around the same core idea: treat server state differently from client state, cache it by key, and revalidate it automatically instead of managing loading and error flags by hand. The difference is scope — SWR is a small, focused caching primitive; TanStack Query is a fuller data-fetching framework with built-in mutation handling, pagination helpers, and devtools.
Why server state needs its own tool
Server state is different from the local state you’d put in useState or a store: it’s owned by something else, can go stale the moment you fetch it, and the same data might be needed by multiple components that didn’t coordinate the request. Handling this by hand means writing useEffect hooks that fetch on mount, track loading and error state, avoid duplicate requests for the same data, and refetch when the user refocuses the tab — a surprising amount of plumbing for what looks like a simple fetch call.
Both libraries solve this with the same pattern: you give a query a key (roughly, an ID for “this piece of server data”) and a function that fetches it. The library caches the result under that key, shares it across every component that asks for the same key, and automatically refetches on a schedule you control — on window focus, on network reconnect, or after a set staleness window. This is the same stale-while-revalidate model that browsers and CDNs use for HTTP caching, applied to application data instead of HTTP responses.
Where they diverge
| TanStack Query | SWR | |
|---|---|---|
| Bundle size | Larger | Smaller, minimal core |
| Mutations | Built-in useMutation with optimistic update helpers | No dedicated mutation API; use mutate() to update the cache manually |
| Pagination / infinite scroll | Built-in useInfiniteQuery | Manual, or via a separate pattern |
| Devtools | Official browser devtools panel | None built in |
| Framework support | React, Vue, Svelte, Solid, Angular | Primarily React (plus a Vue port) |
| Offline / retry behavior | Configurable retry, offline mutation queue | Configurable retry, simpler defaults |
| Mental model | A data-fetching framework | A caching primitive you build on |
SWR’s name is literally the caching strategy it implements — it was designed to be a thin, composable layer that gets out of your way, with mutations handled by directly manipulating the cache through its mutate function. TanStack Query takes a more opinionated, batteries-included stance: it has a dedicated mutation hook with built-in support for optimistic UI updates (show the new state immediately, roll back if the server rejects it), retry queues for offline-then-reconnect scenarios, and query invalidation helpers that make “refetch everything related to this mutation” a one-line call.
Picking one
If your app is read-heavy — dashboards, content sites, anything where most of the interaction is fetching and displaying data rather than mutating it — SWR’s smaller footprint and simpler API are often enough, and the SWR maintainers (the Vercel/Next.js team) keep it well aligned with server-rendering patterns like those in React Server Components.
If your app does a lot of mutations with complex cache invalidation — a project management tool, an admin panel, anything with forms that need to optimistically update lists and detail views together — TanStack Query’s mutation API and query invalidation system save real code. Its devtools panel, which shows every cached query’s state and staleness in the browser, is also a genuine productivity win once an app has more than a handful of query keys to reason about.
Neither library replaces a general state manager like Zustand or Redux — those are for client state that doesn’t come from a server, like UI toggles or form drafts. Using a server-state library for server data and a lightweight store (or just component state) for everything else, rather than stuffing fetched data into a global store, tends to produce less code and fewer stale-data bugs than either “one store for everything” or hand-rolled useEffect fetching.
Server components change the calculus somewhat
Frameworks that fetch data on the server before a component ever reaches the browser — React Server Components being the clearest example — shift some of what these libraries traditionally handled back to the server, since there’s no client-side loading state to manage for data that arrived with the initial render. Both libraries still have a role in that world: anything that needs to refetch after the initial load, respond to user interaction, or stay in sync across client-side navigations still benefits from a client cache, it’s just a smaller slice of the app’s total data fetching than in a fully client-rendered SPA. Neither library is made obsolete by server-first rendering; the boundary of what they’re responsible for just moves.
Migration cost is low either way
Both libraries expose a similar shape — a hook that returns data, isLoading, and error, keyed by a query key — so switching between them later is rarely a full rewrite, more a matter of remapping hook calls and reimplementing whichever mutation or pagination helpers you were relying on. That makes the initial choice lower-stakes than it might feel: starting with SWR for its simplicity and reaching for TanStack Query later if mutation logic gets complicated is a reasonable, low-regret path.
The takeaway
TanStack Query and SWR solve the same problem — caching and revalidating server data instead of hand-rolling fetch effects — but SWR stays a minimal caching primitive while TanStack Query bundles in mutations, pagination, and devtools as first-class features. Read-heavy apps with simple data flows tend to be happy with SWR’s smaller footprint; apps with heavy mutation and cache-invalidation logic usually get more value from TanStack Query’s fuller feature set. Either way, keep server state out of your general-purpose store and let one of these libraries own it.
Tagged
Keep reading
Takina · · 4 min read React Compiler: Automatic Memoization Explained
The React Compiler automatically memoizes components and values at build time, cutting most manual useMemo, useCallback, and React.memo calls.
Takina · · 4 min read useMemo vs useCallback: When to Use Each in React
useMemo caches a computed value; useCallback caches a function reference. Both skip work on re-render, but they memoize different things.
Takina · · 4 min read Next.js App Router vs Pages Router: What's the Difference
The App Router uses React Server Components and nested layouts; the Pages Router is Next.js's original file-based model. How they differ and when to pick each.