TanStack Query v5 vs SWR v3 vs RTK Query 2026
TanStack Query v5 vs SWR v3 vs RTK Query 2026
TL;DR
Server state management has matured into a distinct category from client state. TanStack Query v5 (formerly React Query) is the most feature-complete option — devtools, mutation lifecycle, optimistic updates, prefetching, and SSR support with fine-grained control. SWR v3 by Vercel is the minimalist choice — small bundle, simple API, convention-over-configuration, especially elegant with Next.js. RTK Query is the right choice if you're already using Redux Toolkit — it colocates API definitions with your Redux store and handles loading/error states via slice patterns. In 2026, TanStack Query v5 wins for most use cases outside the Redux and Vercel/SWR ecosystems.
Key Takeaways
- TanStack Query v5: 42M weekly downloads, 43K GitHub stars, v5 introduced simplified mutation API, removed callbacks from
useQuery, TypeScript-first - SWR v3: 30M weekly downloads, 30K GitHub stars, 4KB gzipped, first-class Next.js integration, simple mental model
- RTK Query: ships with Redux Toolkit (15M weekly downloads), zero additional dependencies if you use Redux, endpoints-as-config pattern
- Bundle size: SWR ~4KB < TanStack Query ~13KB < RTK Query ~varies (Redux Toolkit base ~12KB)
- Mutation API: TanStack Query's
useMutationis the most comprehensive; SWR'smutateis simpler but less ergonomic for forms - Framework support: TanStack Query supports React, Vue, Solid, Svelte, Angular; SWR and RTK Query are React-focused
The Server State Problem
Before React Query (now TanStack Query) popularized the concept, most React apps mixed server state and client state in a single Redux store. Server state is fundamentally different: it's owned by a remote server, can be stale, needs cache invalidation, benefits from background refresh, and can fail. Treating it like client state (user preferences, UI toggles) creates complicated loading/error handling and cache invalidation logic.
All three libraries in this comparison are server state libraries — they manage the lifecycle of data fetched from an API with caching, deduplication, and background refresh baked in.
TanStack Query v5
TanStack Query v5 (formerly React Query) released in late 2023 with a significant API overhaul focused on TypeScript ergonomics and simplification.
Key v5 Changes From v4
// v4 (deprecated patterns):
const query = useQuery(['posts', id], () => fetchPost(id), {
onSuccess: (data) => console.log(data), // ❌ removed in v5
onError: (err) => console.error(err), // ❌ removed in v5
})
// v5: unified queryKey + queryFn object syntax
const query = useQuery({
queryKey: ['posts', id],
queryFn: () => fetchPost(id),
})
// Side effects → use useEffect watching data/error
useEffect(() => {
if (query.data) console.log(query.data)
}, [query.data])
The removal of onSuccess/onError callbacks from useQuery was controversial but forced cleaner side-effect patterns.
Basic Data Fetching
import { useQuery, useQueryClient } from '@tanstack/react-query'
interface Post {
id: number
title: string
body: string
userId: number
}
function PostDetail({ postId }: { postId: number }) {
const { data, isLoading, isError, error } = useQuery<Post>({
queryKey: ['posts', postId],
queryFn: async () => {
const res = await fetch(`/api/posts/${postId}`)
if (!res.ok) throw new Error('Failed to fetch post')
return res.json()
},
staleTime: 60_000, // Consider fresh for 60 seconds
gcTime: 5 * 60_000, // Keep in cache for 5 minutes (gcTime replaces cacheTime in v5)
})
if (isLoading) return <Skeleton />
if (isError) return <ErrorMessage error={error} />
return <Article post={data} />
}
Mutations with Optimistic Updates
TanStack Query's mutation API is the most expressive:
const queryClient = useQueryClient()
const updatePost = useMutation({
mutationFn: (updated: Partial<Post>) =>
fetch(`/api/posts/${postId}`, {
method: 'PATCH',
body: JSON.stringify(updated),
}).then(r => r.json()),
// Optimistic update
onMutate: async (updated) => {
await queryClient.cancelQueries({ queryKey: ['posts', postId] })
const previous = queryClient.getQueryData<Post>(['posts', postId])
queryClient.setQueryData<Post>(['posts', postId], (old) => ({
...old!,
...updated,
}))
return { previous } // rollback context
},
onError: (err, updated, context) => {
// Roll back on error
queryClient.setQueryData(['posts', postId], context?.previous)
},
onSettled: () => {
// Always refetch after mutation to sync with server
queryClient.invalidateQueries({ queryKey: ['posts', postId] })
},
})
Prefetching and SSR
// app/posts/[id]/page.tsx (Next.js App Router)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
export default async function PostPage({ params }: { params: { id: string } }) {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['posts', params.id],
queryFn: () => fetchPost(params.id),
})
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<PostDetail postId={params.id} />
</HydrationBoundary>
)
}
SWR v3
SWR (stale-while-revalidate) is Vercel's data fetching library — minimal, fast, and deeply integrated with Next.js. v3 refined the TypeScript experience and added middleware support.
Basic Usage
import useSWR from 'swr'
const fetcher = (url: string) => fetch(url).then(r => r.json())
function Profile({ userId }: { userId: string }) {
const { data, error, isLoading } = useSWR<User>(
`/api/users/${userId}`,
fetcher,
{
refreshInterval: 30_000, // poll every 30s
revalidateOnFocus: true, // refresh when tab regains focus
dedupingInterval: 5_000, // deduplicate same-key requests within 5s
}
)
if (isLoading) return <Spinner />
if (error) return <Error />
return <UserCard user={data} />
}
Mutations and Cache Updates
SWR's mutation approach is simpler but more manual:
import useSWR, { mutate } from 'swr'
function EditProfile() {
const { data: user } = useSWR<User>('/api/user', fetcher)
const updateName = async (newName: string) => {
// Optimistic update — update cache immediately, revalidate after
await mutate(
'/api/user',
async (current: User) => {
await fetch('/api/user', {
method: 'PATCH',
body: JSON.stringify({ name: newName }),
})
return { ...current, name: newName }
},
{ optimisticData: { ...user!, name: newName } }
)
}
return (
<input
defaultValue={user?.name}
onBlur={(e) => updateName(e.target.value)}
/>
)
}
SWR's mutate is elegant for simple updates but becomes verbose for complex optimistic UI patterns.
Middleware
SWR v3 added a proper middleware system:
// Logger middleware
const logger = (useSWRNext: SWRHook) => (key, fetcher, config) => {
const extendedFetcher = (...args: unknown[]) => {
console.log('SWR request:', key)
return fetcher!(...args)
}
return useSWRNext(key, extendedFetcher, config)
}
// Error retry middleware with backoff
const retry = (useSWRNext: SWRHook) => (key, fetcher, config) => {
const retryFetcher = async (...args: unknown[]) => {
try {
return await fetcher!(...args)
} catch (err) {
// Implement exponential backoff
await new Promise(resolve => setTimeout(resolve, 1000))
return fetcher!(...args)
}
}
return useSWRNext(key, retryFetcher, config)
}
// Apply globally
const { data } = useSWR('/api/data', fetcher, { use: [logger, retry] })
RTK Query
RTK Query ships inside Redux Toolkit and is the right choice when your app already uses Redux for client state. It colocates API endpoint definitions with your store configuration.
Setup
// store/api.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const postsApi = createApi({
reducerPath: 'postsApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Post', 'User'],
endpoints: (builder) => ({
getPosts: builder.query<Post[], void>({
query: () => '/posts',
providesTags: ['Post'],
}),
getPost: builder.query<Post, number>({
query: (id) => `/posts/${id}`,
providesTags: (result, error, id) => [{ type: 'Post', id }],
}),
updatePost: builder.mutation<Post, Partial<Post> & { id: number }>({
query: ({ id, ...patch }) => ({
url: `/posts/${id}`,
method: 'PATCH',
body: patch,
}),
invalidatesTags: (result, error, { id }) => [{ type: 'Post', id }],
}),
}),
})
export const { useGetPostsQuery, useGetPostQuery, useUpdatePostMutation } = postsApi
Usage in Components
function PostList() {
const { data: posts, isLoading } = useGetPostsQuery()
const [updatePost, { isLoading: isUpdating }] = useUpdatePostMutation()
const handleUpdate = async (id: number, title: string) => {
try {
await updatePost({ id, title }).unwrap()
// Success — RTK Query auto-invalidates 'Post' tag, re-fetches list
} catch (error) {
console.error('Update failed', error)
}
}
if (isLoading) return <Loading />
return (
<ul>
{posts?.map((post) => (
<PostItem key={post.id} post={post} onUpdate={handleUpdate} />
))}
</ul>
)
}
The invalidatesTags pattern is RTK Query's cache invalidation mechanism — when updatePost succeeds, any query that providesTags: ['Post'] is automatically refetched.
Feature Comparison
| Feature | TanStack Query v5 | SWR v3 | RTK Query |
|---|---|---|---|
| Bundle size (gzip) | ~13KB | ~4KB | ~12KB (with RTK) |
| Weekly downloads | 42M | 30M | part of RTK (15M) |
| Framework support | React, Vue, Solid, Svelte | React only | React only |
| Devtools | ✅ Excellent | ⚠️ Basic | ✅ Redux DevTools |
| Mutation API | ✅ Comprehensive | ⚠️ Simple | ✅ Good |
| Optimistic updates | ✅ First-class | ⚠️ Manual | ✅ Built-in |
| Infinite scroll | ✅ useInfiniteQuery | ✅ useSWRInfinite | ⚠️ Manual |
| Dependent queries | ✅ enabled flag | ✅ null key pattern | ✅ skip flag |
| Background refetch | ✅ | ✅ | ✅ |
| SSR/Hydration | ✅ dehydrate/HydrationBoundary | ✅ SWRConfig fallback | ✅ via Redux hydration |
| Prefetching | ✅ prefetchQuery | ⚠️ preload (limited) | ✅ initiate |
| TypeScript | ✅ First-class | ✅ Good | ✅ First-class |
When to Choose Each
Choose TanStack Query v5 if:
- You're starting a new React (or Vue/Solid/Svelte) project
- You need complex mutation patterns with optimistic UI
- Devtools are important for debugging
- You want the richest query lifecycle control
Choose SWR v3 if:
- Bundle size is critical (mobile, edge)
- Your app is primarily Next.js on the Vercel platform
- Your data fetching is relatively simple (mostly reads)
- You prefer Vercel's conventions and ecosystem
Choose RTK Query if:
- You already use Redux Toolkit for client state
- You want a single store for both server and client state
- Your team knows Redux patterns well
- The endpoints-as-config pattern aligns with your architecture
Infinite Loading and Pagination
All three libraries handle infinite scroll, but with different ergonomics:
TanStack Query — useInfiniteQuery
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 0 }) =>
fetch(`/api/posts?cursor=${pageParam}&limit=20`).then(r => r.json()),
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
initialPageParam: 0,
})
// data.pages is an array of page results
const allPosts = data?.pages.flatMap((page) => page.items) ?? []
SWR — useSWRInfinite
import useSWRInfinite from 'swr/infinite'
const getKey = (pageIndex: number, previousPageData: PageData | null) => {
if (previousPageData && !previousPageData.nextCursor) return null // end
return `/api/posts?cursor=${previousPageData?.nextCursor ?? ''}&limit=20`
}
const { data, size, setSize, isLoading } = useSWRInfinite<PageData>(
getKey,
fetcher
)
const allPosts = data?.flatMap((page) => page.items) ?? []
const loadMore = () => setSize(size + 1)
RTK Query — Infinite Loading Pattern
RTK Query doesn't have a built-in infinite query — you typically use cursor/page state in a parent component and merge results:
// Manual approach for RTK Query infinite scroll
function InfinitePostList() {
const [cursor, setCursor] = useState<string | undefined>()
const [allPosts, setAllPosts] = useState<Post[]>([])
const { data } = useGetPostsQuery({ cursor, limit: 20 })
useEffect(() => {
if (data?.items) setAllPosts((prev) => [...prev, ...data.items])
}, [data])
return (
<>
{allPosts.map((post) => <PostCard key={post.id} post={post} />)}
{data?.nextCursor && (
<button onClick={() => setCursor(data.nextCursor)}>Load More</button>
)}
</>
)
}
TanStack Query's useInfiniteQuery is the most ergonomic; SWR's useSWRInfinite is close; RTK Query requires manual state management for this pattern.
Methodology
- Download data from npm registry API, March 2026
- TanStack Query v5 docs: tanstack.com/query/v5
- SWR v3 docs: swr.vercel.app
- RTK Query docs: redux-toolkit.js.org/rtk-query
- Bundle sizes via bundlephobia.com, March 2026
Compare these packages on PkgPulse: TanStack Query vs SWR.
Related: Zustand vs Jotai vs Nanostores Micro State Management 2026 · Best React Form Libraries 2026