React Query vs SWR vs Apollo Client (2026 Comparison)
Data fetching in React means choosing between TanStack Query (React Query), SWR, and Apollo Client. Each solves the same problem differently. Here's the honest comparison.
Quick Verdict
- TanStack Query — Best overall. Most features, best DevTools, framework-agnostic.
- SWR — Best for simplicity. Minimal API, tiny bundle, by Vercel.
- Apollo Client — Best for GraphQL. Full-featured GraphQL client with cache.
API Comparison
TanStack Query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
function Todos() {
const { data, isLoading, error } = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(r => r.json()),
})
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (newTodo) => fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
}),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})
}
SWR
import useSWR, { useSWRMutation } from 'swr'
const fetcher = (url) => fetch(url).then(r => r.json())
function Todos() {
const { data, isLoading, error } = useSWR('/api/todos', fetcher)
const { trigger } = useSWRMutation('/api/todos', (url, { arg }) =>
fetch(url, { method: 'POST', body: JSON.stringify(arg) })
)
}
Apollo Client
import { useQuery, useMutation, gql } from '@apollo/client'
const GET_TODOS = gql`
query GetTodos {
todos { id title completed }
}
`
function Todos() {
const { data, loading, error } = useQuery(GET_TODOS)
const [addTodo] = useMutation(ADD_TODO, {
refetchQueries: [{ query: GET_TODOS }],
})
}
Key Differences
| Feature | TanStack Query | SWR | Apollo Client |
|---|---|---|---|
| API type | Any (REST, GraphQL, etc.) | Any | GraphQL only |
| Bundle size | ~39KB | ~12KB | ~50KB |
| Cache | Query key-based | URL-based | Normalized |
| DevTools | ✅ Best | ❌ | ✅ Good |
| Mutations | ✅ Full | ✅ Basic | ✅ Full |
| Optimistic updates | ✅ | ✅ | ✅ |
| Infinite scroll | ✅ Built-in | ✅ | ✅ |
| SSR support | ✅ | ✅ | ✅ |
| Offline support | ✅ | ❌ | ✅ |
| Framework-agnostic | ✅ (React, Vue, Solid, Svelte) | ❌ (React only) | ❌ (React only) |
Caching
TanStack Query
Cache by query key. Keys can be arrays:
queryKey: ['todos', { status: 'completed', page: 2 }]
Automatic cache invalidation, stale-while-revalidate, garbage collection. Background refetching. The most configurable caching of the three.
SWR
Cache by URL/key string. Simple and intuitive. Stale-while-revalidate (it's in the name). Less configurable than TanStack Query but covers 90% of needs.
Apollo Client
Normalized cache. Apollo stores entities by __typename and id. When a mutation updates a todo, every query that includes that todo automatically updates.
This is powerful but complex. Cache management can become a maintenance burden.
Winner: Apollo for normalized data. TanStack Query for flexibility and control. SWR for simplicity.
Mutations & Optimistic Updates
TanStack Query
Full mutation support with optimistic updates:
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ['todos'] })
const previous = queryClient.getQueryData(['todos'])
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
return { previous }
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context.previous)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
SWR
Basic mutation support. Optimistic updates via mutate:
const { mutate } = useSWR('/api/todos', fetcher)
// Optimistic update
mutate([...data, newTodo], false)
await addTodo(newTodo)
mutate() // Revalidate
Apollo Client
Optimistic updates via optimisticResponse:
const [addTodo] = useMutation(ADD_TODO, {
optimisticResponse: {
addTodo: { id: 'temp', title: 'New Todo', __typename: 'Todo' },
},
})
Winner: TanStack Query for most control. Apollo for normalized updates.
DevTools
TanStack Query
Best DevTools in the ecosystem. Floating panel shows:
- All active queries
- Cache state (fresh, stale, inactive)
- Refetch triggers
- Mutation status
Install: @tanstack/react-query-devtools
Apollo Client
Apollo DevTools (browser extension). Shows:
- Cache contents (normalized)
- Active queries
- Mutation history
- GraphQL explorer
SWR
No official DevTools. Community tools exist but limited.
Winner: TanStack Query DevTools are the best debugging tool for data fetching.
When to Use Each
Choose TanStack Query When
- REST API or mixed API types
- Want the most features and best DevTools
- Framework-agnostic needs (Vue, Solid, etc.)
- Complex data fetching patterns (dependent queries, pagination)
- Default choice for new projects
Choose SWR When
- Simple data fetching needs
- Bundle size matters
- Using Next.js (same team, great integration)
- Prefer minimal API surface
- Don't need advanced mutation patterns
Choose Apollo Client When
- GraphQL API (it's purpose-built for this)
- Need normalized caching
- Complex data relationships
- GraphQL subscriptions for real-time
- Full GraphQL ecosystem (codegen, fragments)
FAQ
Can I use TanStack Query with GraphQL?
Yes. Use it with graphql-request or any GraphQL client. You lose normalized caching but gain TanStack Query's DevTools and features.
Is SWR too simple?
For most apps, no. SWR covers 90% of data fetching needs. Only switch to TanStack Query if you need advanced patterns.
Is Apollo Client too heavy?
At 50KB, it's the largest. If you're not using GraphQL, it's not worth it. For GraphQL, it's the standard.
What about server components?
React Server Components fetch data on the server. You still need client-side fetching for interactive features. TanStack Query and SWR work well alongside RSC.
Bottom Line
TanStack Query is the default choice for data fetching in 2026. Most features, best DevTools, works with any API type. SWR is the minimalist alternative — perfect when you want simplicity. Apollo Client is the right choice only if you're using GraphQL.
For new React projects: TanStack Query. For simple apps on Next.js: SWR. For GraphQL: Apollo Client.