React SWR: Stale-While-Revalidate Data Fetching (2026)
SWR is Vercel's lightweight data-fetching library for React, built around the HTTP cache-control stale-while-revalidate strategy: return cached (stale) data immediately, then revalidate in the background. It's smaller than TanStack Query and pairs naturally with Next.js, making it the go-to choice for Next.js projects that need client-side data fetching without the full TanStack Query feature set.
Table of Contents
useSWR Basics
npm install swr
import useSWR from 'swr'
// Fetcher function — SWR calls this with the key
const fetcher = (url: string) => fetch(url).then(r => r.json())
function UserProfile({ id }) {
const { data, error, isLoading, isValidating } = useSWR(
`/api/users/${id}`, // Key — also the URL
fetcher
)
if (isLoading) return <Skeleton />
if (error) return <p>Error: {error.message}</p>
return (
<div>
{isValidating && <span>Refreshing...</span>}
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
)
}
SWR automatically revalidates on:
- Window focus (user returns to the tab)
- Network reconnect
- Component remount
- Interval (if
refreshIntervalis set)
Global Configuration
// app/providers.tsx or _app.tsx
import { SWRConfig } from 'swr'
const globalFetcher = async (url: string) => {
const res = await fetch(url)
if (!res.ok) {
const error = new Error('Fetch failed') as any
error.status = res.status
error.info = await res.json()
throw error
}
return res.json()
}
export function SWRProvider({ children }) {
return (
<SWRConfig
value={{
fetcher: globalFetcher,
revalidateOnFocus: true,
revalidateOnReconnect: true,
revalidateIfStale: true,
dedupingInterval: 2000, // Dedupe requests within 2s
errorRetryCount: 3,
errorRetryInterval: 5000,
onError: (error) => {
if (error.status !== 403 && error.status !== 404) {
reportError(error)
}
},
}}
>
{children}
</SWRConfig>
)
}
Conditional Fetching
// Pass null key to disable fetching
function UserPosts({ userId }) {
// Only fetch if userId exists
const { data } = useSWR(userId ? `/api/users/${userId}/posts` : null, fetcher)
return <ul>{data?.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}
// Dependent fetching — fetch user first, then their projects
function UserProjects({ email }) {
const { data: user } = useSWR(`/api/users?email=${email}`, fetcher)
// Only runs when user.id is available
const { data: projects } = useSWR(user?.id ? `/api/projects?userId=${user.id}` : null, fetcher)
return <ProjectList projects={projects} />
}
// Complex key — any JSON-serializable value works as a key
const { data } = useSWR(
{ url: '/api/products', params: { category, page, sort } },
({ url, params }) => fetch(url + '?' + new URLSearchParams(params)).then(r => r.json())
)
Mutation and Revalidation
import useSWR, { useSWRConfig } from 'swr'
function DeleteButton({ postId }) {
const { mutate } = useSWRConfig()
async function handleDelete() {
await fetch(`/api/posts/${postId}`, { method: 'DELETE' })
// Revalidate posts list — triggers fresh fetch
mutate('/api/posts')
}
return <button onClick={handleDelete}>Delete</button>
}
// Bound mutate — from useSWR directly
function PostTitle({ id }) {
const { data, mutate } = useSWR(`/api/posts/${id}`, fetcher)
async function updateTitle(newTitle) {
// Update cache immediately, revalidate in background
await mutate(
fetch(`/api/posts/${id}`, {
method: 'PATCH',
body: JSON.stringify({ title: newTitle }),
}).then(r => r.json()),
{ optimisticData: { ...data, title: newTitle }, rollbackOnError: true }
)
}
return <h1 onClick={() => updateTitle('New Title')}>{data?.title}</h1>
}
Optimistic Updates
function TodoList() {
const { data: todos, mutate } = useSWR('/api/todos', fetcher)
async function addTodo(text: string) {
const newTodo = { id: Date.now(), text, done: false }
await mutate(
// Async mutation — runs the actual API call
fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ text }),
}).then(r => r.json()),
{
// Show optimistic data immediately
optimisticData: todos ? [...todos, newTodo] : [newTodo],
// Rollback if API call fails
rollbackOnError: true,
// Revalidate after mutation settles
revalidate: true,
// Populate cache with API response
populateCache: (result, currentData) =>
currentData ? [...currentData, result] : [result],
}
)
}
return (
<div>
{todos?.map(todo => <TodoItem key={todo.id} todo={todo} />)}
<button onClick={() => addTodo('New task')}>Add Todo</button>
</div>
)
}
Infinite Loading
import useSWRInfinite from 'swr/infinite'
function PostFeed() {
const PAGE_SIZE = 10
const { data, size, setSize, isLoading, isValidating } = useSWRInfinite(
(pageIndex, previousPageData) => {
// No more pages
if (previousPageData && !previousPageData.length) return null
return `/api/posts?page=${pageIndex + 1}&limit=${PAGE_SIZE}`
},
fetcher
)
const posts = data ? data.flat() : []
const isEmpty = data?.[0]?.length === 0
const isReachingEnd = isEmpty || (data && data[data.length - 1]?.length < PAGE_SIZE)
return (
<div>
{posts.map(post => <PostCard key={post.id} post={post} />)}
{!isReachingEnd && (
<button
onClick={() => setSize(size + 1)}
disabled={isValidating}
>
{isValidating ? 'Loading...' : 'Load More'}
</button>
)}
</div>
)
}
TypeScript
import useSWR, { SWRConfiguration } from 'swr'
interface User {
id: string
name: string
email: string
role: 'admin' | 'user'
}
interface ApiError {
message: string
status: number
}
// Type the generic params: <Data, Error>
function useUser(id: string) {
return useSWR<User, ApiError>(
id ? `/api/users/${id}` : null,
fetcher
)
// data: User | undefined
// error: ApiError | undefined
}
// Reusable typed hook factory
function createSWRHook<T>(key: string, config?: SWRConfiguration) {
return () => useSWR<T>(key, fetcher, config)
}
const useSettings = createSWRHook<Settings>('/api/settings', {
revalidateOnFocus: false,
})
Suspense Mode
import { Suspense } from 'react'
import useSWR from 'swr'
function UserProfile({ id }) {
// suspense: true — component suspends while loading, no isLoading check needed
const { data: user } = useSWR(`/api/users/${id}`, fetcher, { suspense: true })
return <div>{user.name}</div>
}
// Wrap in Suspense + ErrorBoundary
function Page() {
return (
<ErrorBoundary fallback={<p>Error</p>}>
<Suspense fallback={<Skeleton />}>
<UserProfile id="123" />
</Suspense>
</ErrorBoundary>
)
}
SWR vs TanStack Query
| Feature | SWR | TanStack Query |
|---|---|---|
| Bundle size | ~4KB | ~13KB |
| Mutations | Manual (mutate) | First-class (useMutation) |
| Devtools | Third-party | Official devtools |
| Offline support | Basic | Full (persistQueryClient) |
| Best for | Simple fetching, Next.js projects | Complex mutations, large apps |