Context
SWR takes its name from the HTTP pattern it implements — stale-while-revalidate: show the cached value immediately and fire a revalidation in parallel, updating the UI when the response arrives. TanStack Query (formerly React Query) grew out of the same problem — fetching async data in React components without rewriting the same loading, error, cache, and background-revalidation logic on every screen — but was built as a more complete cache layer, with its own client, query lifecycle, and a set of primitives for mutations that SWR leaves out of the core.
The most visible difference isn't technical, it's scope: SWR was designed by Vercel to stay small and do one thing well — fetch and revalidate. TanStack Query was designed to grow alongside the application, covering normalized cache, infinite scroll, optimistic mutations, and devtools, and today has adapters beyond React — Vue, Svelte, Solid, and Angular share the same core. That difference in ambition already shows up at install time: TanStack Query requires a QueryClientProvider wrapping the component tree before any useQuery works, while SWR runs with no provider required, with an implicit global cache keyed by key.
When to choose TanStack Query
TanStack Query pays off when the application does more than read data: it also writes, and those writes need to invalidate or update the cache predictably. The useMutation hook is a first-class citizen, with callbacks (onSuccess, onError, onSettled) and direct integration with queryClient to invalidate the affected queries:
import { useMutation, useQueryClient } from "@tanstack/react-query";
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: updateTodo,
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["todos"] }),
});
That centralized queryClient is what powers the more advanced features: useInfiniteQuery for pagination and infinite scroll without manually reimplementing page accumulation, route prefetching, cache hydration from the server, and official devtools that show the state of each query — fresh, stale, fetching, inactive — in real time during development. The cost is conceptual: to get value from the tool, the team needs to understand query keys, stale time, cache time, and the difference between invalidating and refetching a query, concepts that don't exist in SWR's vocabulary.
When to choose SWR
SWR fits when what the application needs is, in practice, to fetch data and keep it fresh — without building a layer of mutations, pagination, or elaborate hydration on top of that. A single hook covers most cases:
import useSWR from "swr";
const { data, error, isLoading } = useSWR("/api/todos", fetcher);
By default, SWR already revalidates on window focus, on network reconnect, and at configurable intervals, which covers much of what teams normally reimplement manually with raw fetch. For mutations, the library offers two paths: the global mutate() function, which updates a specific key's cache from outside the component, and the useSWRMutation addon, which packages a mutation as its own hook when the global pattern isn't enough. Neither one tries to be as complete as TanStack Query's useMutation — and that economy of concepts is exactly what keeps SWR simple to audit even in a large codebase, since any developer can follow the data flow without learning a cache-specific vocabulary.
Verdict
Both libraries solve the same fetching pain with quality and active maintainers behind them — Vercel in SWR's case, the TanStack team on the other. What really changes is how much cache infrastructure the application already needs today, not a year from now: if the product has write-heavy screens, paginated lists, and teams used to cache devtools, TanStack Query's larger surface pays off early. If what exists is a handful of read calls that just need to stay up to date without drama, adding a dedicated cache client for that is solving a problem the application doesn't have yet — and that's where SWR's economy of concepts is worth more than any extra feature.