TanStack Query vs SWR: Managing Server State in React
Practice4 minInnovialabs editorial team
In short
Both libraries cache server data and refresh it automatically; SWR is lighter and simpler for read-heavy apps, while TanStack Query gives more control over mutations, invalidation and debugging in large apps.
The short answer
SWR is a small, focused hook for fetching and revalidating data. TanStack Query is a fuller toolkit: cache configuration, mutations with rollback, query invalidation, infinite lists and official devtools. If your app mostly reads data, SWR is enough. If users create, edit and delete a lot, TanStack Query saves you from writing that logic yourself.
Why server state is not client state
Client state belongs to the browser: an open modal, a selected tab, form input. You own it, and it is always current.
Server state is different:
- it lives on the server and you only hold a copy of it;
- the copy can become stale at any moment because other users change data;
- fetching it is asynchronous and can fail;
- several components often need the same data at once.
Putting server data into useState or a global store means writing loading flags, error handling, deduplication, refetching and cache expiry by hand. Both libraries solve exactly this.
Basic usage
// SWR
const { data, error, isLoading } = useSWR(`/api/orders/${id}`, fetcher)
// TanStack Query
const { data, error, isPending } = useQuery({
queryKey: ['orders', id],
queryFn: () => fetchOrder(id),
})
In SWR the key is usually the URL. In TanStack Query the key is an array, which makes it easy to invalidate groups of queries, for example everything starting with ['orders'].
Side-by-side comparison
| Aspect | SWR | TanStack Query |
|---|---|---|
| Caching | Shared cache by key, deduplication of parallel requests | Shared cache with staleTime and garbage collection time per query |
| Revalidation | On mount, window focus, reconnect, interval | Same triggers, configurable per query |
| Mutations | mutate() and useSWRMutation | useMutation with onMutate, onError, onSettled |
| Invalidation | By key or key matcher function | invalidateQueries by key prefix |
| Pagination | useSWRInfinite, keepPreviousData option | useInfiniteQuery, placeholderData |
| Devtools | No official devtools | Official devtools panel |
| Size and API | Smaller, fewer concepts | Larger, more options |
Caching and revalidation
SWR follows the stale-while-revalidate idea: show cached data instantly, fetch fresh data in the background, swap it in. This works well by default with almost no configuration.
TanStack Query adds an explicit notion of freshness. staleTime says how long data counts as fresh (no refetch during that period), and the garbage collection time says how long unused data stays in memory. This lets you treat a currency rate list differently from a live order status.
Mutations
This is where the libraries diverge most.
- In SWR you call your API, then
mutate(key)to refresh, or pass optimistic data tomutate. It is simple but you coordinate related keys yourself. - In TanStack Query
useMutationhas lifecycle callbacks. A typical optimistic update: save the previous cache inonMutate, write the new value, roll back inonError, and callinvalidateQueriesinonSettled.
const mutation = useMutation({
mutationFn: updateOrder,
onSettled: () => queryClient.invalidateQueries({ queryKey: ['orders'] }),
})
Pagination and infinite lists
Both support "load more" lists: useSWRInfinite and useInfiniteQuery. For classic numbered pages, both can keep the previous page visible while the next one loads, so the table does not flicker. TanStack Query's version handles cursors and "has next page" logic a bit more explicitly.
Devtools
TanStack Query ships an official devtools panel: you see every query, its key, status, freshness and data, and you can refetch or invalidate manually. In large apps this is a real time saver when debugging "why did this not update". SWR relies on logging or community tools.
How to choose
- Small app, landing page with a few API calls, dashboards that mostly read — SWR. Less code, fewer concepts.
- Admin panels, CRMs, apps with many forms and lists that must stay in sync — TanStack Query. Invalidation by key prefix and structured mutations pay off.
- Next.js App Router with data loaded in server components — you may need neither for initial data; use a client library only where data changes on the client.
- Team already uses one of them — stay consistent; both are mature.
Common mistakes
- Copying fetched data into
useStateand losing automatic updates. - Unstable keys, such as objects recreated on every render in SWR, causing extra requests.
- Forgetting to invalidate related lists after a mutation.
- Setting aggressive refetch intervals everywhere instead of only where data really changes often.