Skip to content
Innovialabs

TanStack Query vs SWR: Managing Server State in React

Practice4 min

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

AspectSWRTanStack Query
CachingShared cache by key, deduplication of parallel requestsShared cache with staleTime and garbage collection time per query
RevalidationOn mount, window focus, reconnect, intervalSame triggers, configurable per query
Mutationsmutate() and useSWRMutationuseMutation with onMutate, onError, onSettled
InvalidationBy key or key matcher functioninvalidateQueries by key prefix
PaginationuseSWRInfinite, keepPreviousData optionuseInfiniteQuery, placeholderData
DevtoolsNo official devtoolsOfficial devtools panel
Size and APISmaller, fewer conceptsLarger, 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 to mutate. It is simple but you coordinate related keys yourself.
  • In TanStack Query useMutation has lifecycle callbacks. A typical optimistic update: save the previous cache in onMutate, write the new value, roll back in onError, and call invalidateQueries in onSettled.
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 useState and 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.

FAQ

Yes. Keep server data in the query library and use Redux or Zustand only for real client state like UI settings. Duplicating server data in a global store is the usual source of bugs.

We can help with this

If you'd rather get the result than figure it all out yourself — this is what we do every day.

Discuss a project

Read next

Have an idea or a task? Tell us about it

We'll reply within one business day and propose a solution. The consultation is free.