· tanstack-query / swr / react

TanStack Query vs SWR — React Server State in 2026

TanStack Query leads on features, devtools, and momentum. SWR leads on bundle size and zero-config simplicity. Here is when each choice is right for your app.

By

1,907 words · 10 min read

Use TanStack Query if your app has complex cache invalidation, background mutations, or a team that will live inside devtools. Use SWR if you want a Vercel-native library that does one thing well and never gets in your way. Both are actively maintained. Neither is wrong. The question is what you’re optimizing for.

Who this is for

React developers choosing between the two for a new project, or weighing a migration. If you’re already using one in production and it isn’t causing pain, keep it — switching for its own sake costs more than it saves.

What we tested

TanStack Query v5.100.10 and SWR v2.4.1, both as of May 2026. Data comes from official changelogs, npm registry, bundlephobia, and the GitHub repos. Download figures are from npm stats. This is a comparison article, not a benchmark — where methodology matters, we say so.

Why this comparison still matters in 2026

Most “TanStack Query vs SWR” articles floating around compare react-query v3 to SWR v1. That was a different contest. Both libraries have shipped major versions since then, and the gap between them has changed.

TanStack Query v5 landed in October 2023 and has been actively maintained since — the repo saw a commit on 2026-05-17. SWR v2 landed in December 2022 and reached v2.4.1 in February 2026. The API surfaces are different enough from their predecessors that older advice often points you at patterns that no longer exist.

React Server Components and Server Actions have also reshuffled the deck. Where each library fits into an App Router app is not obvious from v1-era documentation.

On the momentum question: TanStack Query downloads hit 12.3 million/week in May 2026, roughly 2.5× SWR’s 4.85 million/week. That gap has widened year over year since 2024. SWR’s user base is healthy but narrower, concentrated in the Vercel ecosystem.

Quick verdict

DimensionTanStack Query v5.100.10SWR v2.4.1
Bundle (minzipped)~13.4 KB~4.2 KB
npm downloads/week12.3M4.85M
GitHub stars49,44432,382
TypeScriptTyped errors, generics throughoutFully typed, simpler inference
DevToolsBuilt-in floating panelCommunity browser extension
Next.js / RSC fitDehydration + pending-query streamingNative Vercel product, simpler prefetch pattern
Cache controlstaleTime, gcTime, networkMode, maxPagesrevalidateOnFocus, dedupingInterval
Mutation DXuseMutation — verbose but preciseuseSWRMutation — cleaner for simple writes
Learning curveSteeper (~20 config options)Low: one hook, sensible defaults

Deep dive: TanStack Query v5

The API became a single-object signature

v5 dropped the positional overloads. Every hook now takes one options object:

// v4
useQuery(['user', id], fetchUser, { staleTime: 60_000 })

// v5
useQuery({ queryKey: ['user', id], queryFn: fetchUser, staleTime: 60_000 })

A codemod ships with v5 to handle the migration mechanically. The change is a breaking one, but it makes query config portable — you can extract it into a queryOptions() helper and share it across components and server-side calls without duplicating options.

cacheTime became gcTime

This rename clarifies what the setting actually controls. gcTime is when unused, inactive queries get garbage-collected from memory. staleTime is how long fetched data is considered fresh before a background refetch happens. Conflating the two was the most common source of confusing behavior in v4.

Typed errors

v5 defaults error to Error instead of unknown. Combined with the new throwOnError option (which replaces the deprecated useErrorBoundary), you get full type safety in error paths without casting.

Suspense hooks

useSuspenseQuery, useSuspenseInfiniteQuery, and useSuspenseQueries are stable in v5. They guarantee non-nullable data at the type level, which means no if (!data) return null inside Suspense trees. The component either throws a promise (suspends) or renders with data. No third state to guard against.

Pending-query streaming (v5.40+)

This is the v5 feature most likely to matter for App Router apps. The library can now serialize in-flight queries and dehydrate them for the client. A Next.js page can start streaming HTML immediately while a query is still in flight on the server. The client hydrates into the same pending state — no waterfall, no flash of loading, no double fetch.

DevTools

@tanstack/react-query-devtools is a floating panel lazy-loaded in development (tree-shaken in production). It shows every query key, its status, staleTime countdown, cache data, and network events. If you’re debugging cache invalidation across a large component tree, this is the tool that makes it tractable.

Framework coverage

v5 ships adapters for React, Vue, Svelte, Angular, Solid, and (as of 2026) Lit. The core is framework-agnostic. For teams that aren’t exclusively React, this matters.

Deep dive: SWR v2

useSWRMutation — the gap that v1 left open

SWR v1 had no first-class mutation primitive. Teams worked around it by calling the global mutate after a plain fetch, which worked but left the loading and error state wiring to you. v2 closes this with useSWRMutation:

const { trigger, isMutating } = useSWRMutation('/api/users', createUser, {
  optimisticData: (currentData) => [...currentData, optimisticUser],
  populateCache: true,
  rollbackOnError: true,
})

Explicit trigger invocation, first-class isMutating state, built-in optimistic lifecycle. For write-heavy apps, this is what makes v2 a serious option where v1 was a workaround.

Filter-based global mutate

v2 lets you invalidate cache entries by predicate:

// Invalidate every key that starts with '/api/user'
mutate((key) => typeof key === 'string' && key.startsWith('/api/user'))

Previously you had to pass the exact key. For apps with dynamic routes, this is the difference between surgical invalidation and a full cache wipe.

isLoading vs isValidating

v1 had only isValidating, which is true during any fetch — initial, background revalidation, or focus-triggered refresh. v2 adds isLoading, which is true specifically when there is no data yet and a first fetch is in flight. This matches the mental model most UIs need: show a skeleton only on the first load, not on every background refresh.

The Vercel advantage

SWR is a Vercel product. Next.js team dogfoods it, and the App Router SSR pattern is straightforward:

// app/users/page.tsx — Server Component
export default async function UsersPage() {
  const users = await fetchUsers()
  return (
    <SWRConfig value={{ fallback: { '/api/users': users } }}>
      <UserList />
    </SWRConfig>
  )
}

// UserList.tsx — Client Component
'use client'
function UserList() {
  const { data } = useSWR('/api/users', fetcher)
  return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}

The client component renders immediately from the fallback, then revalidates in the background. No QueryClient to instantiate. No provider boilerplate.

The limit of this pattern: SWR hooks are client-only. For pending-query streaming across RSC/RCC boundaries, TanStack Query has more infrastructure.

The simplicity case

SWR’s core mental model: one key, one fetcher, one hook. No QueryClient. No Provider required by default. For a team that primarily needs to cache GET requests and revalidate on focus, SWR takes an afternoon to learn and a long time to outgrow. That’s a genuine feature, not a limitation.

Head-to-head: same task

Task: Fetch a user profile, show a loading state, handle errors, and refetch on window focus.

TanStack Query v5

import { useQuery, QueryClient, QueryClientProvider } from '@tanstack/react-query'

const queryClient = new QueryClient({
  defaultOptions: { queries: { staleTime: 60_000, gcTime: 300_000 } },
})

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <UserProfile userId="123" />
    </QueryClientProvider>
  )
}

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

  if (isPending) return <Spinner />
  if (isError) return <ErrorMessage error={error} />
  return <div>{data.name}</div>
}

SWR v2

import useSWR from 'swr'

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

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

  if (isLoading) return <Spinner />
  if (error) return <ErrorMessage error={error} />
  return <div>{data.name}</div>
}

SWR is ~40% less code for this use case. No provider setup, no QueryClient, no defaultOptions to configure. TanStack Query’s verbosity pays off when you need fine-grained cache control, typed errors, devtools, or cross-component cache sharing with explicit lifetime semantics.

If you’re exploring either API from scratch, Cursor’s inline autocomplete is particularly effective at filling in queryKey generics and suggesting the right hook options — both libraries have dense option surfaces and typed inference that autocomplete handles well.

When to choose which

Choose TanStack Query v5 when

  • Complex cache invalidation — you need to invalidate multiple query keys after a mutation, or orchestrate between related caches.
  • Background mutations at scaleuseMutation with onMutate/onError/onSettled lifecycle is battle-tested and composable.
  • TypeScript rigor matters to your team — typed errors, typed queryFn return types, queryOptions() helpers for portable config.
  • You need devtools — the built-in panel is meaningfully better than anything SWR offers.
  • Next.js App Router + streaming — dehydrating pending queries and streaming them to the client requires TanStack Query’s infrastructure.
  • Multi-framework teams — same library across React, Vue, Angular, Svelte.
  • tRPC — tRPC’s official React adapter is built on TanStack Query. If you use tRPC, you’re already using it.

Choose SWR v2 when

  • Vercel / Next.js Pages Router — SWR is the native recommendation and the docs pattern is the simplest path.
  • Bundle size matters — 4.2 KB vs 13.4 KB minzipped is a real difference for bundle-sensitive apps.
  • Straightforward CRUD — if your data model is “fetch this URL and show it,” SWR’s one-hook model is unbeatable for that scope.
  • Team is new to React state management — lower cognitive overhead, fewer footguns, faster to productive.
  • Rapid prototyping — no boilerplate, sensible defaults, works without configuration.

Verdict

TanStack Query is the bigger, more capable library. SWR is the leaner, quieter one. They’re not directly competing for the same user.

If you’re on a Next.js/Vercel stack with simple data needs: SWR. If you’re building a complex dashboard, a B2B product with heavy mutations and cache coordination, or a multi-framework app: TanStack Query. If you’re starting from scratch and aren’t sure: pick TanStack Query and grow into it. Migrating off SWR later is more work than learning TanStack Query upfront.

Both libraries cover server state only. For client-side state management alongside either one, see our Zustand vs Redux Toolkit comparison.

Caveats

  • Numbers in the “Quick verdict” table are pinned to May 2026. Download counts and star counts drift — verify at npm before citing them.
  • No runtime performance benchmark is included here. Both libraries use useSyncExternalStore internally (React 18 concurrent-safe) and behave equivalently at the React scheduler level. No independent 2025–2026 benchmark with solid methodology exists as of writing.
  • We tested no native mobile targets. React Native behavior may differ, particularly for offline/background sync.
  • This article contains an affiliate link for Cursor. We use Cursor. It doesn’t change the verdict on either library.

References