React

TanStack Query vs SWR for React Data Fetching in 2026

Compare TanStack Query vs SWR for React data fetching: cache invalidation, mutation handling, and offline support explained to help you pick the right library.

By Laxaar Engineering Team Jun 23, 2026 11 min read
TanStack Query vs SWR for React Data Fetching in 2026

Most React apps leak complexity through their data layer. Components accumulate loading flags, stale-data bugs ship because a mutation didn't update the right cache key, and the codebase ends up with three different ways to fetch the same resource. The root cause is usually the same: the team reached for useEffect and useState because they were familiar, not because they were the right tool for server state.

TanStack Query (formerly React Query) and SWR are the two libraries that solve this class of problem in React. Both give you declarative data fetching, automatic background revalidation, and deduplication. They share roughly 80% of their surface area. The other 20% is where real projects feel the difference.

We've shipped apps with both at the Laxaar team, and the choice has mattered enough to be worth writing down. TanStack Query vs SWR is not a question of which is "better" in the abstract. It's about which fits the shape of your problem.

What you'll learn

What server state management actually means

Server state is data that lives on a server and is only borrowed by the client. It's asynchronous, can be stale the moment it arrives, can be changed by other users, and needs to be re-fetched periodically. This is fundamentally different from client state (form values, UI toggles, modal open/closed) which you own entirely.

useEffect + useState can technically fetch data. The problem is that it gives you none of the plumbing that server state actually needs: deduplication of identical in-flight requests, background refetching on window focus, cache lifetime management, or retry on failure. You end up writing all of that yourself, inconsistently, across every component that touches remote data.

Both TanStack Query and SWR replace that pattern with a cache-and-revalidate model. A query key identifies a piece of server data. The library fetches it, caches the result, returns it instantly on subsequent mounts, and revalidates in the background. Components re-render only when the data they care about changes.

The philosophical difference: SWR is built around simplicity and a small API surface. TanStack Query is built around control: it exposes more knobs, and it expects you to use them on complex data relationships. At Laxaar, we default to TanStack Query on greenfield projects precisely because complex cache relationships tend to appear sooner than teams expect.

How caching works in each library

SWR's cache is keyed by the first argument to useSWR. The key is a string or a function that returns a string (returning null pauses the fetch). The cache is global and shared across component instances.

// SWR basic usage (swr v2)
import useSWR from 'swr'

const fetcher = (url: string) => fetch(url).then(r => r.json())

function UserProfile({ userId }: { userId: string }) {
  const { data, error, isLoading } = useSWR(`/api/users/${userId}`, fetcher)

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Failed to load</div>
  return <div>{data.name}</div>
}

TanStack Query uses queryKey arrays instead of strings. This matters: ['users', userId] and ['users', userId, { role: 'admin' }] are different cache entries, and TanStack Query lets you invalidate all queries that start with ['users'] in one call. The hierarchical key structure is the foundation of its cache invalidation model.

// TanStack Query basic usage (v5)
import { useQuery } from '@tanstack/react-query'

function UserProfile({ userId }: { userId: string }) {
  const { data, error, isLoading } = useQuery({
    queryKey: ['users', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
  })

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Failed to load</div>
  return <div>{data.name}</div>
}

Both libraries configure stale time (how long cached data is considered fresh) and garbage collection time (how long unused cache entries are kept). SWR calls these dedupingInterval and there's no explicit garbage collection; inactive entries stay until the cache is cleared. TanStack Query's staleTime and gcTime (renamed from cacheTime in v5) give you explicit control over both.

Cache invalidation and query relationships

This is where the libraries diverge most sharply, and it's the section that usually determines which one fits your app.

SWR uses mutate to invalidate or update the cache. You call mutate('/api/users/1') to mark that key as stale and trigger a revalidation. It works cleanly for simple cases. The limitation surfaces when you need to invalidate a family of queries (all user listings, all paginated variants), because SWR's string keys don't have a native prefix-match mechanism. You'd need to track keys yourself or use the global mutate function with a filter predicate.

// SWR cache mutation after an update
import { useSWRConfig } from 'swr'

function UpdateUser() {
  const { mutate } = useSWRConfig()

  const handleUpdate = async (userId: string, data: Partial<User>) => {
    await fetch(`/api/users/${userId}`, { method: 'PATCH', body: JSON.stringify(data) })
    // Invalidate this specific key
    mutate(`/api/users/${userId}`)
    // Invalidate the list — must know the exact key
    mutate('/api/users')
  }
}

TanStack Query's invalidateQueries accepts a partial key and matches everything that starts with it. Update a user, invalidate ['users'], and every cached query whose key begins with ['users'] is marked stale and revalidated. That includes the user detail, every page of the user list, and any filtered view. This single behavior justifies TanStack Query's larger footprint in apps with complex relational data.

// TanStack Query invalidation by prefix
import { useMutation, useQueryClient } from '@tanstack/react-query'

function useUpdateUser() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: ({ userId, data }: { userId: string; data: Partial<User> }) =>
      fetch(`/api/users/${userId}`, { method: 'PATCH', body: JSON.stringify(data) }).then(r => r.json()),
    onSuccess: (updatedUser) => {
      // Invalidate everything under ['users']
      queryClient.invalidateQueries({ queryKey: ['users'] })
      // Optimistically set the specific record
      queryClient.setQueryData(['users', updatedUser.id], updatedUser)
    },
  })
}

Mutation handling compared

SWR's useSWRMutation is a good fit when mutations are simple and you want minimal API surface. You get a trigger function, loading/error state, and the ability to optimistically update the cache. What it doesn't have out of the box: mutation queuing, retry configuration per-mutation, or lifecycle hooks (onMutate, onSuccess, onError, onSettled) that let you coordinate side effects.

TanStack Query's useMutation ships all of those. onMutate fires before the server call, which is where you implement optimistic updates. onError receives the rollback context you returned from onMutate. onSettled always runs so you can invalidate regardless of outcome. For forms with complex optimistic UI or multi-step mutation flows, this lifecycle is worth its weight.

Optimistic updates deserve special attention because they're where most teams hit unexpected complexity. With SWR, you call mutate(key, optimisticData, { optimistic: true }) and handle rollback manually on error. With TanStack Query, the onMutate / onError context pattern keeps rollback logic co-located with the mutation definition, which is cleaner at scale.

Offline support and background sync

SWR revalidates on reconnect by default (revalidateOnReconnect: true). It does not queue mutations made while offline or retry them when connectivity returns. If a user submits a form while offline, the request fails and it's your responsibility to handle it.

TanStack Query's offline behavior is more capable. With the networkMode: 'offlineFirst' option (introduced in v4, refined in v5), queries and mutations that fail due to network unavailability are paused and automatically retried when connectivity returns. The persistQueryClient plugin lets you persist the entire query cache to localStorage or IndexedDB, so the app loads with stale data rather than a loading skeleton after a refresh.

For a standard SaaS or e-commerce app, SWR's default reconnect revalidation is enough. For apps targeting users on unreliable connections (field-service tools, apps used on mobile data), TanStack Query's persistence and offline-first mutation queuing are meaningful features, not just nice-to-haves.

Bundle size, TypeScript, and DevTools

Bundle size is the honest trade-off in this comparison. SWR is roughly 4KB gzipped. TanStack Query is around 13KB gzipped. For most production apps the difference is negligible next to images and fonts, but it's real and worth acknowledging for performance-sensitive contexts.

TypeScript support is excellent in both libraries. TanStack Query v5 rewrote its generics to require fewer explicit type annotations. useQuery infers the data type from the queryFn return type without manual annotation in most cases. SWR's generics are clean and have been stable for several major versions.

The DevTools difference is where TanStack Query earns developer-experience points. The @tanstack/react-query-devtools panel shows every cache entry, its staleness status, the last-fetch timestamp, observer count, and the full query key. Debugging "why is this component showing stale data" is straightforward. SWR has no official DevTools panel, which means debugging cache behavior is done through console logs or custom tooling.

Side-by-side comparison

FeatureTanStack Query v5SWR v2
Cache key modelArray-based, hierarchicalString or function
Prefix invalidationYes, built-inNo (manual filter)
Mutation lifecycle hooksonMutate / onError / onSettledBasic trigger only
Optimistic update rollbackContext-based, co-locatedManual
Offline mutation queuingYes (networkMode)No
Cache persistenceYes (persistQueryClient)Community plugins
DevToolsOfficial panelNone official
Bundle size (gzip)~13KB~4KB
Pagination helpersusePaginatedQuery, useInfiniteQueryuseSWRInfinite
Learning curveModerateLow

Our honest take at Laxaar: SWR is the right default for apps where data relationships are shallow and the team values a minimal API. TanStack Query is the right choice when you have relational cache invalidation needs, optimistic UI, or offline requirements. Reaching for TanStack Query because it's "more powerful" on a simple blog frontend is over-engineering; reaching for SWR on a complex CRM because it's simpler is technical debt.

Frequently Asked Questions

Can I use SWR and TanStack Query in the same project?

Technically yes, but don't. Both libraries manage a global cache and you'd be doubling the bundle size while splitting your data layer across two mental models. Pick one and apply it consistently. If you're migrating from SWR to TanStack Query, do it incrementally per route rather than mixing them long-term.

Does TanStack Query work with React Server Components?

TanStack Query is a client-side cache. In the Next.js App Router model, you'd prefetch data in a Server Component using the dehydrate / HydrationBoundary pattern: fetch on the server, serialize the cache, rehydrate on the client. SWR supports the same pattern with fallbackData. Neither library replaces server-side fetch with cache() for purely server-rendered data. Both handle the client cache layer on top of it.

Which one is better for paginated and infinite-scroll lists?

Both handle this well. TanStack Query's useInfiniteQuery has a more explicit API (getNextPageParam, getPreviousPageParam) that makes bidirectional pagination straightforward. SWR's useSWRInfinite is simpler but requires more manual management of the page index. For infinite scroll, TanStack Query's approach tends to produce less boilerplate as the pagination logic gets complex.

How do I handle dependent queries (fetch B only after A succeeds)?

In SWR, pass null as the key to pause a fetch: useSWR(userId ? /api/users/${userId} : null, fetcher). In TanStack Query, use the enabled option: useQuery({ queryKey: ['profile', userId], queryFn: fetchProfile, enabled: !!userId }). Both work. TanStack Query's enabled is more explicit and composable: you can combine multiple conditions without string manipulation.

Is SWR still actively maintained in 2026?

Yes. Vercel maintains SWR and ships regular updates. It's the data-fetching library used in many of Vercel's own Next.js examples. The slower feature velocity compared to TanStack Query is intentional. SWR's value proposition is stability and a small API, not an ever-expanding feature set.


The data layer is one of the few architectural decisions that shows up in every feature sprint, not just the first one. Get it wrong and you're chasing stale-state bugs six months later. If you're weighing this choice on a new project and want a second set of eyes on the architecture, the Laxaar custom software development team reviews data layers regularly. You can see how we've applied these patterns in practice on our portfolio, or get in touch to talk through your specific setup.

Working on something like this?

Get a fixed scope, timeline, and price within one business day — no obligation.

ReactData FetchingServer State Management
Grow your business with us

Take your business to the next level.

Tell us what you're building. We'll come back inside one business day with a fixed scope, timeline, and team — or an honest “this isn't a fit”.

ENGINEERING PHILOSOPHY

Code is useless if it's not comprehensible to those who maintain it. We write code the next person can actually understand.