React Query vs SWR in 2026: What I Actually Use and Why
Source: Dev.to
The 30‑Second Answer
SWR – Simpler API, smaller bundle (~4 KB), made by Vercel – native Next.js fit. Covers about 80 % of use cases.
React Query (TanStack Query) – More features, more control, larger bundle (~13 KB). 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 }
}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 }
}SWR’s entire API fits in your head after one afternoon.
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)
},
})
}More verbose, but 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)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,
})Infinite scroll
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 0 }) => fetchPosts({ cursor: pageParam }),
getNextPageParam: (lastPage) => lastPage.nextCursor,
})SWR offers useSWRInfinite; React Query’s version is generally 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
}// 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
}No loading flicker on first paint, and the 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 / prefetching
- Heavy infinite scroll
- Non‑Next.js React (framework‑agnostic)
Pick one and commit before you’re two months in.
I ship SaaS tools at whoffagents.com.