SWR
SWR is Vercel's open-source data fetching library for React. Its name comes from the HTTP cache-control strategy stale-while-revalidate: show cached (stale) data immediately, then fetch fresh data in the background, and update the UI when the new data arrives. SWR is lighter and requires less configuration than TanStack Query, making it an excellent choice for Next.js applications and simpler data requirements.
Installation
npm install swr
Basic Usage
useSWR takes a key (typically the URL) and a fetcher function. The fetcher is any function that accepts the key and returns a Promise:
import useSWR from 'swr'
// Define a reusable fetcher function
const fetcher = (url) => fetch(url).then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
})
function UserProfile({ userId }) {
const { data, error, isLoading } = useSWR(
userId ? `/api/users/${userId}` : null, // null key = don't fetch
fetcher
)
if (isLoading) return <p>Loading…</p>
if (error) return <p>Error: {error.message}</p>
if (!data) return null
return (
<div>
<h2>{data.name}</h2>
<p>{data.email}</p>
</div>
)
}Return Values
data— the fetched data, orundefinedwhile loading.error— the error thrown by the fetcher, orundefinedon success.isLoading— true only on the initial load (no cached data yet).isValidating— true whenever a request is in-flight (initial or background).mutate— function to update the cache or trigger a revalidation.
Global Configuration
Use SWRConfig to set global defaults — fetcher, revalidation options, and error retries — so you don't repeat them on every call:
import { SWRConfig } from 'swr'
const globalFetcher = (url) =>
fetch(url).then(res => {
if (!res.ok) throw new Error(`Request failed: ${res.status}`)
return res.json()
})
function App() {
return (
<SWRConfig
value={{
fetcher: globalFetcher,
revalidateOnFocus: true, // refetch when window regains focus
revalidateOnReconnect: true, // refetch when network reconnects
dedupingInterval: 2000, // deduplicate requests within 2s window
errorRetryCount: 3, // retry failed requests 3 times
refreshInterval: 0, // 0 = no polling (set ms for live data)
}}
>
<YourApp />
</SWRConfig>
)
}Mutation & Cache Invalidation
SWR's mutate function updates the cache locally and/or triggers a revalidation. It's available from the useSWR return value (scoped to one key) or as a global mutate (affects any key):
import useSWR, { mutate } from 'swr'
function PostList() {
const { data: posts, mutate } = useSWR('/api/posts', fetcher)
async function deletePost(id) {
// Optimistic update: remove from local cache immediately
mutate(
posts.filter(p => p.id !== id),
false // false = don't revalidate yet
)
try {
await fetch(`/api/posts/${id}`, { method: 'DELETE' })
mutate() // trigger a fresh fetch to confirm the deletion
} catch {
mutate() // revert by refetching on error
}
}
return (
<ul>
{posts?.map(post => (
<li key={post.id}>
{post.title}
<button onClick={() => deletePost(post.id)}>Delete</button>
</li>
))}
</ul>
)
}
// From anywhere — invalidate a key without having access to the hook instance
async function createNewPost(data) {
await fetch('/api/posts', { method: 'POST', body: JSON.stringify(data) })
mutate('/api/posts') // global mutate: revalidate the posts list
}useSWRMutation for Explicit Mutations
For operations like form submissions that should not run automatically, useSWRMutation provides an explicit trigger function:
import useSWRMutation from 'swr/mutation'
async function createPost(url, { arg }) {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(arg),
})
if (!res.ok) throw new Error('Failed to create post')
return res.json()
}
function CreatePostForm() {
const { trigger, isMutating, error } = useSWRMutation(
'/api/posts',
createPost
)
async function handleSubmit(e) {
e.preventDefault()
const form = new FormData(e.target)
await trigger({ title: form.get('title'), body: form.get('body') })
e.target.reset()
}
return (
<form onSubmit={handleSubmit}>
<input name="title" placeholder="Title" required />
<textarea name="body" placeholder="Body" />
<button type="submit" disabled={isMutating}>
{isMutating ? 'Saving…' : 'Create Post'}
</button>
{error && <p>Error: {error.message}</p>}
</form>
)
}Infinite Pagination with useSWRInfinite
import useSWRInfinite from 'swr/infinite'
const PAGE_SIZE = 10
function getKey(pageIndex, previousPageData) {
if (previousPageData && !previousPageData.length) return null // end of list
return `/api/posts?page=${pageIndex + 1}&limit=${PAGE_SIZE}`
}
function InfinitePostList() {
const { data, size, setSize, isLoading } = useSWRInfinite(getKey, fetcher)
const posts = data ? data.flat() : []
const hasMore = data && data[data.length - 1]?.length === PAGE_SIZE
if (isLoading) return <p>Loading…</p>
return (
<div>
{posts.map(post => <p key={post.id}>{post.title}</p>)}
{hasMore && (
<button onClick={() => setSize(size + 1)}>Load more</button>
)}
</div>
)
}SWR vs TanStack Query
Feature | SWR | TanStack Query |
|---|---|---|
Bundle size | ~4 kB gzipped | ~13 kB gzipped |
Configuration | Minimal | Highly configurable |
Mutations | useSWRMutation (simpler) | useMutation (more features) |
Optimistic updates | Manual mutate() | onMutate / setQueryData |
DevTools | None built-in | Dedicated devtools panel |
SSR / Next.js | First-class support | Good, needs SWR workaround |
Infinite queries | useSWRInfinite | useInfiniteQuery |
Subscriptions | No | No |
Offline support | Basic | Full (v5) |
When to Choose SWR
Next.js projects — SWR is made by Vercel and integrates seamlessly with Next.js data patterns.
Simpler applications — if you primarily need GET requests with caching and deduplication, SWR is less setup.
Smaller bundle budgets — SWR is roughly 3x smaller than TanStack Query.
Already using Vercel / SWR ecosystem — consistent tooling choice.
Choose TanStack Query instead when you need advanced mutation workflows, offline support, the DevTools panel, or fine-grained cache control.