DEV Community

Atlas Whoff
Atlas Whoff

Posted on

React Query vs SWR in 2026: What I Actually Use and Why

I've shipped production apps with both. Here's the honest breakdown.

The 30-Second Answer

  • SWR: Simpler API, smaller bundle (~4KB), made by Vercel — native Next.js fit. Covers 80% of use cases.
  • React Query (TanStack Query): More features, more control, bigger bundle (~13KB). Built for complex data-heavy apps.

Next.js app with moderate data fetching: SWR.
Complex mutations, infinite scroll, cache seeding: React Query.

SWR in Practice

import useSWR from 'swr'

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

export function useUser(id: string) {
  const { data, error, isLoading, mutate } = useSWR(`/api/users/${id}`, fetcher)
  return { user: data, isLoading, isError: !!error, refetch: mutate }
}
Enter fullscreen mode Exit fullscreen mode

SWR's entire API fits in your head after one afternoon.

Mutations:

import useSWRMutation from 'swr/mutation'

async function updateUser(url: string, { arg }: { arg: { name: string } }) {
  return fetch(url, { method: 'PATCH', body: JSON.stringify(arg) }).then(r => r.json())
}

export function useUpdateUser(id: string) {
  const { trigger, isMutating } = useSWRMutation(`/api/users/${id}`, updateUser)
  return { updateUser: trigger, isUpdating: isMutating }
}
Enter fullscreen mode Exit fullscreen mode

React Query in Practice

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'

export function useUser(id: string) {
  return useQuery({
    queryKey: ['user', id],
    queryFn: () => fetch(`/api/users/${id}`).then(r => r.json()),
    staleTime: 5 * 60 * 1000,
  })
}

export function useUpdateUser() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: ({ id, data }: { id: string; data: { name: string } }) =>
      fetch(`/api/users/${id}`, { method: 'PATCH', body: JSON.stringify(data) }).then(r => r.json()),
    onSuccess: (_, { id }) => {
      queryClient.invalidateQueries({ queryKey: ['user', id] })
    },
    onMutate: async ({ id, data }) => {
      await queryClient.cancelQueries({ queryKey: ['user', id] })
      const previousUser = queryClient.getQueryData(['user', id])
      queryClient.setQueryData(['user', id], (old: any) => ({ ...old, ...data }))
      return { previousUser }
    },
    onError: (_, { id }, context) => {
      queryClient.setQueryData(['user', id], context?.previousUser)
    },
  })
}
Enter fullscreen mode Exit fullscreen mode

More verbose. The control is there when you need it.

Where React Query Wins

Cache seeding from list → detail:

// No extra network request when navigating from list to detail
queryClient.setQueryData(['user', user.id], user)
Enter fullscreen mode Exit fullscreen mode

Dependent queries:

const { data: user } = useQuery({ queryKey: ['user', userId], queryFn: fetchUser })
const { data: projects } = useQuery({
  queryKey: ['projects', user?.orgId],
  queryFn: () => fetchProjects(user!.orgId),
  enabled: !!user?.orgId,
})
Enter fullscreen mode Exit fullscreen mode

Infinite scroll:

const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: ({ pageParam = 0 }) => fetchPosts({ cursor: pageParam }),
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})
Enter fullscreen mode Exit fullscreen mode

SWR has useSWRInfinite — React Query's version is more ergonomic.

The Hybrid I Actually Use

Server Components for initial data (no library needed), SWR for client-side live data:

// Server Component — plain fetch, no library
export default async function UserPage({ params }: { params: { id: string } }) {
  const user = await db.user.findUnique({ where: { id: params.id } })
  return <UserClient initialUser={user} />
}

// Client Component — SWR for live updates
'use client'
import useSWR from 'swr'

export function UserClient({ initialUser }: { initialUser: User }) {
  const { data: user } = useSWR(`/api/users/${initialUser.id}`, fetcher, {
    fallbackData: initialUser,  // Hydrate from server, no loading flash
    refreshInterval: 30_000,
  })
  return <UserForm user={user} />
}
Enter fullscreen mode Exit fullscreen mode

No loading flicker on first paint. Client bundle stays small.

Decision Framework

Use SWR if:

  • Next.js app (especially App Router)
  • Simple read-heavy data fetching
  • Bundle size matters
  • Small team, less cache management complexity

Use React Query if:

  • Complex mutations with optimistic updates
  • Need manual cache seeding/prefetch
  • Heavy infinite scroll
  • Non-Next.js React (React Query is framework-agnostic)

Pick one and commit before you're two months in.


I ship SaaS tools at whoffagents.com.

Top comments (0)