Choosing between TanStack Query and SWR is a decision every React developer hits around the time their app grows past a few fetch calls. Both solve server-state caching, but they diverge sharply in philosophy, and picking the wrong one costs you real development time later.
In this post, I'll break down the TanStack Query vs SWR comparison based on what actually matters in production: devtools, mutation handling, and how each library scales with complex data. I've used both extensively, and the differences are more than cosmetic.
TanStack Query vs SWR: The Key Differences
The core split is simple: SWR is a fetch hook with caching; TanStack Query is a server-state management library. That distinction drives everything else.
Mutation handling is where you feel it first. SWR's mutate is a manual broadcast — you call it and hope every component revalidates correctly. TanStack Query gives you useMutation with built-in state machines, automatic retries, and rollback support. If your app does anything beyond a simple POST, this matters.
Devtools are the second big divider. TanStack Query ships with a full browser extension that shows every query, its status, and lets you inspect cached data in real time. SWR has a basic timeline but nothing close. When you're debugging a stale cache in production, this gap is painful.
Garbage collection is the third difference. TanStack Query removes unused queries after 5 minutes by default; SWR keeps them in memory indefinitely unless you manually delete. For long-running SPAs, that's a memory leak you'll need to handle yourself.
When to Use TanStack Query
Choose TanStack Query when your app has complex interdependent data — dashboards, e-commerce, anything with multiple components reading overlapping server state.
The killer feature is query cancellation and deduplication built into the core. Here's a meaningful difference in practice:
// TanStack Query — automatic dedup, cancellation, and retry
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: async ({ signal }) => {
const res = await fetch(`/api/users/${userId}`, { signal });
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
},
staleTime: 60_000, // cache for 1 minute
retry: 3,
});
// SWR — you must wire up AbortController yourself
const { data } = useSWR(`/api/users/${userId}`, async (url) => {
const res = await fetch(url);
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
});
Notice TanStack Query handles the AbortSignal for you. SWR doesn't. In a real app with users clicking fast, that's the difference between smooth UX and a cascade of aborted requests.
When to Use SWR
Choose SWR when you have simple, mostly-read-only data — a blog, a landing page, a small internal tool. If your API surface is a handful of GET endpoints, SWR's minimalism is a feature, not a limitation.
SWR shines for lightweight integration. It's ~2KB gzipped vs TanStack Query's ~13KB. If you're shipping a tiny widget or a micro-frontend where bundle size is critical, that matters.
SWR also handles optimistic UI with less boilerplate for single mutations:
// SWR — optimistic update in a few lines
const { mutate } = useSWRConfig();
const updateProfile = async (newData) => {
// Optimistically update the cache
await mutate('/api/profile', newData, {
optimisticData: newData,
rollbackOnError: true,
});
};
That's clean. In TanStack Query, you'd write the same with onMutate, onError, and onSettled callbacks — more code, more control. If you don't need the control, SWR wins.
TanStack Query or SWR: Which One Should You Pick?
The honest answer: pick TanStack Query unless you have a concrete reason not to. Here's why in one line — TanStack Query's feature set covers every SWR use case, but SWR can't cover TanStack Query's advanced scenarios without significant manual work.
If your app is a static site with a few fetches, SWR's simplicity wins. If you're building anything with mutations, pagination, or cross-component cache invalidation, TanStack Query will save you from reinventing wheels SWR leaves out.
My Take
I've migrated two production apps from SWR to TanStack Query, and I've never migrated the other way. The breaking point was always the same: when mutations start interacting with each other, SWR's manual mutate calls become a tangled web of useEffect dependencies. TanStack Query's declarative invalidation (invalidateQueries) is the difference between code you can reason about and code you're scared to touch.
If you're starting a new project today, default to TanStack Query. The learning curve is steeper, but you'll spend that saved time debugging data consistency issues later.
The one thing that makes this decision obvious: if your app has ever needed to sync two components after a mutation, you already know the answer — TanStack Query. If it hasn't, SWR is fine until it isn't.