Skip to main content
BlogWeb Dev

React Query vs SWR: Which to Use in 2026

A side-by-side comparison of TanStack Query and SWR for data fetching in Next.js — with real code examples for mutations, caching, and SSR.

Matthews Wong

Software Engineer

June 20258 min read
React Query vs SWR: Which to Use in 2026

Photo by Unsplash

Both React Query (now TanStack Query) and SWR solve the same problem: managing remote state in React without the prop drilling and useEffect spaghetti that results from manual fetch calls. I've used both on production projects. SWR powers a simpler public-facing Next.js site; React Query runs the dashboard for an ERP internal tool with complex mutations, optimistic updates, and offline support. My take: SWR is a better default for simple apps, React Query is worth its size for complex ones.

DimensionSWRReact Query
Bundle sizeAbout 4KB minified and gzippedAbout 13KB minified and gzipped
MutationsManual mutate() calls, no built-in mutation APIDedicated useMutation hook with optimistic updates
Caching modelSimple stale-while-revalidate cacheConfigurable cache with query keys, staleTime, and gcTime
Offline supportBasic revalidate-on-reconnectBuilt-in offline queue and mutation retry
DevToolsNo official devtools panelDedicated devtools for inspecting cache and query state
Best fitSimple data fetching in public-facing pagesComplex dashboards with mutations and offline needs

The Core Difference: Philosophy

SWR (Stale-While-Revalidate) is from Vercel and optimized for the Vercel/Next.js ecosystem. Its philosophy is minimal API surface — one useSWR hook covers most use cases. React Query from TanStack is more opinionated about server state management, ships with DevTools, and has first-class support for mutations, pagination, infinite queries, and offline mode. The bundle size difference (~4KB for SWR vs ~13.5KB for React Query) matters on content sites but not dashboards.

What They Share

Both libraries deduplicate requests — if three components on the same page call useSWR('/api/user') or useQuery(['user']), only one HTTP request fires. Both revalidate on window focus and reconnect. Both support background refetching, custom fetcher functions, and global configuration. Both integrate well with Next.js App Router's server components.

When SWR Wins

SWR wins for content-heavy sites where you mainly need to fetch and display data with occasional simple mutations. Its API is smaller to learn, it integrates perfectly with the Vercel ecosystem, and for simple CRUD without complex cache invalidation needs, the ergonomics are cleaner. If your data requirements are 'show a list, show a detail, maybe do a simple update', SWR is plenty and costs you 9KB less JavaScript.

Feature Comparison: React Query vs SWR

┌─────────────────────────┬──────────────┬──────────────┐
│ Feature                 │ React Query  │     SWR      │
├─────────────────────────┼──────────────┼──────────────┤
│ Bundle size (min+gzip)  │   ~13.5 kB   │   ~4.2 kB    │
│ Mutations               │   ✅ Built-in │   ⚠️ Manual  │
│ Optimistic updates      │   ✅ Easy     │   ✅ Possible │
│ Infinite scroll         │   ✅ Built-in │   ✅ Built-in │
│ Offline support         │   ✅ Yes      │   ⚠️ Partial  │
│ DevTools                │   ✅ Official │   ❌ None     │
│ Server State (SSR)      │   ✅ Full     │   ✅ Full     │
│ Background refetch      │   ✅ Yes      │   ✅ Yes      │
│ Request deduplication   │   ✅ Yes      │   ✅ Yes      │
│ Prefetching             │   ✅ Yes      │   ⚠️ Manual  │
│ Learning curve          │   Medium     │   Low        │
│ Best for                │ Complex apps │ Simple fetch │
└─────────────────────────┴──────────────┴──────────────┘

In Next.js App Router, prefetch server-side data using QueryClient.prefetchQuery() in your Server Component, then pass the dehydrated state to HydrationBoundary in the Client Component. This gives you zero-loading-state on first render while still getting all React Query's client-side cache management. SWR has an equivalent fallback prop for the same pattern.

Mutations: Where React Query Pulls Ahead

SWR mutations require manual cache updates via the mutate() function — you tell SWR what the new cache value should be. React Query's useMutation hook has an onMutate / onError / onSettled lifecycle that makes optimistic updates, rollback on error, and cache invalidation a structured pattern rather than ad-hoc logic. For ERP dashboards where users edit records and need immediate UI feedback with rollback on network failure, React Query's mutation model is significantly cleaner.

Infinite Queries and Pagination

Both libraries support infinite scrolling with useInfiniteQuery (React Query) and useSWRInfinite (SWR). React Query's implementation is more ergonomic — the pages array and fetchNextPage() function handle most pagination patterns without custom logic. SWR requires more manual wiring but gives you more control over the data merging strategy. For a paginated table vs an infinite scroll feed, both work well.

// ─── React Query: full mutation with optimistic update ───
import { useMutation, useQueryClient } from "@tanstack/react-query"

function useUpdateProduct() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: (data: UpdateProductDto) =>
      fetch(`/api/products/${data.id}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      }).then((r) => r.json()),

    // Optimistic update
    onMutate: async (newProduct) => {
      await queryClient.cancelQueries({ queryKey: ["products", newProduct.id] })
      const prev = queryClient.getQueryData(["products", newProduct.id])
      queryClient.setQueryData(["products", newProduct.id], newProduct)
      return { prev }
    },

    onError: (_err, newProduct, ctx) => {
      queryClient.setQueryData(["products", newProduct.id], ctx?.prev)
    },

    onSettled: (_data, _err, vars) => {
      queryClient.invalidateQueries({ queryKey: ["products", vars.id] })
      queryClient.invalidateQueries({ queryKey: ["products"] })
    },
  })
}

// ─── SWR: fetching + manual mutation ───
import useSWR, { mutate } from "swr"

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

function useProducts() {
  const { data, error, isLoading } = useSWR("/api/products", fetcher, {
    revalidateOnFocus: true,
    dedupingInterval: 5000,
  })

  const updateProduct = async (id: string, payload: Partial<Product>) => {
    // Optimistic update with SWR
    await mutate(
      "/api/products",
      async (current: Product[]) => {
        const updated = await fetch(`/api/products/${id}`, {
          method: "PATCH",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(payload),
        }).then((r) => r.json())
        return current.map((p) => (p.id === id ? updated : p))
      },
      { optimisticData: (current) =>
          current?.map((p) => (p.id === id ? { ...p, ...payload } : p)) }
    )
  }

  return { data, error, isLoading, updateProduct }
}

Server State vs Client State

The key insight both libraries share: server state is fundamentally different from client state. Client state (UI toggles, form inputs, modal open/closed) belongs in useState or a state manager like Zustand. Server state (database records, API responses) belongs in React Query or SWR. Mixing them — storing fetched data in useState — leads to stale data, race conditions, and synchronization bugs.

Using both libraries in the same codebase doubles the bundle cost and creates two separate cache layers that don't know about each other. Pick one at project start and stick to it. If you inherit a codebase with both, migrate everything to React Query (it's more capable) and remove SWR. Keeping both 'temporarily' almost always becomes permanent.

Integrating with Next.js App Router

In Next.js App Router, Server Components handle the initial fetch — no React Query or SWR needed for the first paint. React Query and SWR come in for client-side interactions after hydration: refreshing data on user actions, polling for live updates, and mutations. Wrap your Client Components in a QueryClientProvider (React Query) or SWRConfig (SWR) in your root layout. Use the dehydration pattern to pass server-fetched data into the client cache.

My Recommendation

For a new Next.js project: start with SWR. It covers 80% of use cases with a smaller bundle and simpler mental model. Migrate to React Query if you need DevTools to debug cache state, are doing optimistic mutations with rollback, have complex dependent queries, or need offline-first behavior. For ERP dashboards, admin panels, and data-heavy applications: start with React Query. The investment in learning the mutation lifecycle pays off within the first sprint.

Sources & Further Reading

FAQ

Frequently Asked Questions

Short answers to what readers ask most about this topic.

What is the main difference between React Query and SWR?

SWR from Vercel is designed for a minimal API surface — one useSWR hook covers most use cases — and is optimized for the Vercel/Next.js ecosystem. React Query (TanStack Query) is more opinionated about server state management, ships with DevTools, and has first-class support for mutations, pagination, infinite queries, and offline mode. The bundle size difference (~4 KB for SWR vs ~13.5 KB for React Query) matters on content sites but not on dashboards.

When should I choose SWR over React Query?

SWR is the better default for content-heavy sites where you mainly fetch and display data with occasional simple mutations. Its API is smaller to learn and integrates perfectly with the Vercel ecosystem. If your data requirements are 'show a list, show a detail, maybe do a simple update', SWR is plenty and costs 9 KB less JavaScript.

Why does React Query handle mutations better than SWR?

SWR mutations require manual cache updates via the mutate() function — you explicitly tell SWR what the new cache value should be. React Query's useMutation hook provides an onMutate / onError / onSettled lifecycle that makes optimistic updates, rollback on error, and cache invalidation a structured pattern rather than ad-hoc logic. For ERP dashboards where users edit records and need immediate UI feedback with rollback on network failure, React Query's mutation model is significantly cleaner.

How do React Query and SWR integrate with Next.js App Router?

In Next.js App Router, Server Components handle the initial fetch — no React Query or SWR is needed for the first paint. After hydration, React Query and SWR handle client-side interactions such as refreshing data on user actions, polling for live updates, and mutations. You can prefetch data server-side using QueryClient.prefetchQuery() and pass the dehydrated state to HydrationBoundary in the Client Component for zero-loading-state on first render; SWR has an equivalent fallback prop for the same pattern.

Is it okay to use both React Query and SWR in the same project?

No — using both libraries in the same codebase doubles the bundle cost and creates two separate cache layers that don't know about each other. You should pick one at project start and stick to it. If you inherit a codebase with both, the recommendation is to migrate everything to React Query and remove SWR, because keeping both 'temporarily' almost always becomes permanent.