Articles

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.

Takina Takina · · 5 min read
An abstract illustration of API data flowing between systems

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 QuerySWR
Bundle sizeLargerSmaller, minimal core
MutationsBuilt-in useMutation with optimistic update helpersNo dedicated mutation API; use mutate() to update the cache manually
Pagination / infinite scrollBuilt-in useInfiniteQueryManual, or via a separate pattern
DevtoolsOfficial browser devtools panelNone built in
Framework supportReact, Vue, Svelte, Solid, AngularPrimarily React (plus a Vue port)
Offline / retry behaviorConfigurable retry, offline mutation queueConfigurable retry, simpler defaults
Mental modelA data-fetching frameworkA 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.

Takina 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.

#React #JavaScript #Web Development
Takina 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.

#JavaScript #React #Web Development