swr
SWR is a React hook for reading remote data. You give useSWR a key (usually the URL) and a fetcher function, and it hands back data, error, isLoading and isValidating. Behind that one call sits a shared cache keyed by the key you passed, so two components asking for the same thing make one request. The name comes from the stale-while-revalidate strategy: it shows whatever is in the cache immediately, refetches in the background, then rerenders with the fresh result. Out of the box it also deduplicates requests inside a time window, refetches when the tab regains focus or the network comes back, retries failures with backoff, and lets you write to the cache optimistically before the server confirms. Companion hooks cover paginated lists (useSWRInfinite), explicit POST-style mutations (useSWRMutation) and live sources such as websockets (useSWRSubscription). It is built by the Next.js team at Vercel but has no Next.js dependency.
The smallest sensible way to get cached, deduplicated, self-refreshing reads into a React app, and it stays out of your way. Reach for TanStack Query instead the moment mutations, cache invalidation rules or debugging tools become the hard part.
Use it if
- You want read-heavy data fetching in React with almost no ceremony: one hook per resource, no store to configure, no query client to mount
- Several components on the page need the same resource and you want one request and one shared cache entry rather than prop drilling the data down
- You want the tab to refresh itself: revalidation on window focus, on network reconnect and on an interval are options rather than code you write
- Bundle size matters and you do not want a data layer that costs more than your UI code: the main entry is around 5.5 KB gzipped with two tiny dependencies
- You are already on Next.js and want the client-side cache to match how the framework thinks about stale data
- You need a full query and mutation layer with devtools, query invalidation by predicate, request cancellation, prefetching helpers and offline persistence: TanStack Query has all of that and SWR deliberately does not
- Your app is write-heavy: SWR's model is a cache of GET results, and coordinating a form-heavy CRUD app means a lot of manual mutate calls with populateCache and revalidate flags
- You are on the Next.js App Router and most of your data is loaded in server components: you may not need a client cache at all, and the 2.5 feature for handing server-fetched data to client hooks (cacheData) is labelled experimental
- You cannot afford surprise traffic: revalidateOnFocus, revalidateOnReconnect and revalidateOnMount are all on by default, so an expensive endpoint gets hit every time a user tabs back unless you turn them off
- You want in-flight requests aborted when the key changes: SWR does not do that, it just ignores the stale response, so any real cancellation is AbortController code you write inside your own fetcher
- You are not on React: SWR is React hooks only, so a Vue, Svelte or Solid app needs a different library
Setup reality
npm install swr and you are done: two small dependencies (dequal and use-sync-external-store), no build config, TypeScript types included. React is a peer dependency accepting 16.11 through 19, so package managers with strict peer resolution will complain on a React canary. The real setup is the parts nobody installs for you. SWR ships no fetcher, so every project writes one, and the version everyone writes first is wrong: fetch does not reject on a 404 or a 500, so unless your fetcher explicitly throws on a non-ok response, error stays undefined and data becomes your error page's JSON. Put the fetcher, and your revalidation defaults, in a single SWRConfig at the app root rather than repeating them per hook. Keys must be stable and serializable: an inline object literal is fine because SWR serializes it, but a value that changes identity and content on every render (a new Date, a fresh AbortController) turns into a new cache entry and an infinite refetch loop. On the App Router, any file using these hooks needs the client directive.
Patterns
Read a resource with a fetcher that actually throwsbasic-fetch
import useSWR from 'swr'
const fetcher = async (url) => {
const res = await fetch(url)
if (!res.ok) {
const err = new Error('Request failed')
err.status = res.status
err.info = await res.text()
throw err
}
return res.json()
}
function Profile() {
const { data, error, isLoading } = useSWR('/api/user', fetcher)
if (error) return <p>failed: {error.status}</p>
if (isLoading) return <p>loading</p>
return <p>hello {data.name}</p>
}fetch resolves on 404 and 500, so without the explicit throw your error branch never runs and data holds the error body. isLoading means the first load with nothing cached; isValidating is true for background refreshes too.
Set the fetcher and revalidation defaults onceglobal-config
'use client'
import { SWRConfig } from 'swr'
export function Providers({ children }) {
return (
<SWRConfig
value={{
fetcher,
revalidateOnFocus: false,
dedupingInterval: 5000,
errorRetryCount: 3,
}}
>
{children}
</SWRConfig>
)
}revalidateOnFocus defaults to true, which means a refetch every time the user switches tabs back. Turn it off globally and re-enable it on the few hooks that need live data.
Skip or chain requests with a null keyconditional-fetch
const { data: user } = useSWR('/api/me', fetcher)
// waits until user exists, then fetches
const { data: orders } = useSWR(
user ? `/api/users/${user.id}/orders` : null,
fetcher
)A null, undefined or false key means no request and isLoading stays false. Pass a function instead of a value if computing the key can throw while the dependency is still undefined.
Key on more than a URLarray-key
const { data } = useSWR(
['/api/orders', { status, page }],
([url, params]) => fetcher(url + '?' + new URLSearchParams(params))
)SWR serializes array and object keys by value, so a fresh literal each render is fine. What is not fine is putting a function, class instance or Date in the key: those never compare equal and you get a refetch loop.
Write to the cache before the server answersoptimistic-update
const { data: todos, mutate } = useSWR('/api/todos', fetcher)
async function addTodo(text) {
await mutate(
async () => {
const res = await fetch('/api/todos', { method: 'POST', body: JSON.stringify({ text }) })
return (await res.json()).todos
},
{
optimisticData: [...todos, { id: 'temp', text, pending: true }],
rollbackOnError: true,
populateCache: true,
revalidate: false,
}
)
}populateCache: true uses whatever the async function returns as the new cache value, so it has to return the same shape the fetcher would. Set revalidate: false only when you trust that shape, otherwise leave it on.
Run a POST on demand with useSWRMutationtrigger-mutation
import useSWRMutation from 'swr/mutation'
async function createOrder(url, { arg }) {
const res = await fetch(url, { method: 'POST', body: JSON.stringify(arg) })
if (!res.ok) throw new Error('create failed')
return res.json()
}
function NewOrder() {
const { trigger, isMutating, error, reset } = useSWRMutation('/api/orders', createOrder)
return (
<button disabled={isMutating} onClick={() => trigger({ sku: 'A1' })}>
{isMutating ? 'saving' : 'save'}
</button>
)
}Unlike useSWR, this fires nothing on mount and nothing on focus. trigger rejects by default, so wrap it in try/catch or pass throwOnError: false and read error off the hook.
Load more pages with useSWRInfiniteinfinite-pagination
import useSWRInfinite from 'swr/infinite'
const PAGE_SIZE = 20
const getKey = (index, previous) => {
if (previous && previous.length < PAGE_SIZE) return null // reached the end
return `/api/orders?page=${index + 1}&limit=${PAGE_SIZE}`
}
function Orders() {
const { data, size, setSize, isValidating } = useSWRInfinite(getKey, fetcher)
const rows = data ? data.flat() : []
const done = data && data[data.length - 1]?.length < PAGE_SIZE
return (
<>
{rows.map(r => <Row key={r.id} {...r} />)}
<button disabled={done || isValidating} onClick={() => setSize(size + 1)}>more</button>
</>
)
}By default every page revalidates whenever any page does, which on page 20 is 20 requests. Pass revalidateFirstPage: false and revalidateAll: false for long lists, and parallel: true if page keys do not depend on the previous page.
Poll an endpoint on an intervalpolling-refresh
const { data } = useSWR('/api/job/42', fetcher, {
refreshInterval: (latest) => (latest?.status === 'running' ? 2000 : 0),
refreshWhenHidden: false,
})refreshInterval accepts a function of the latest data, so polling can stop itself once the job finishes. Polling pauses on a hidden tab unless you opt into refreshWhenHidden.
Avoid a blank screen while a search key changeskeep-previous-data
const { data, isLoading } = useSWR(
query ? `/api/search?q=${encodeURIComponent(query)}` : null,
fetcher,
{ keepPreviousData: true }
)Without this, changing the key resets data to undefined and the list flashes empty on every keystroke. With it, the previous results stay on screen and isLoading tells you a newer set is coming.
Warm the cache before navigationpreload-on-hover
import { preload } from 'swr'
<Link
href={`/orders/${id}`}
onMouseEnter={() => preload(`/api/orders/${id}`, fetcher)}
>
view
</Link>preload writes into the same cache the hook reads, so the destination page renders from cache. It only runs on the client; calling it during a server render is a no-op.
Revalidate a group of keys after a writeinvalidate-many-keys
import { useSWRConfig } from 'swr'
const { mutate } = useSWRConfig()
await saveOrder(order)
await mutate(
(key) => typeof key === 'string' && key.startsWith('/api/orders'),
undefined,
{ revalidate: true }
)The global mutate from useSWRConfig accepts a filter function; the mutate returned by a hook is bound to that one key. Keys built as arrays arrive here as arrays, so a string-only check silently misses them.
Fetch something that never changesimmutable-data
import useSWRImmutable from 'swr/immutable'
const { data: countries } = useSWRImmutable('/api/countries', fetcher)
// equivalent to useSWR(key, fetcher, {
// revalidateIfStale: false,
// revalidateOnFocus: false,
// revalidateOnReconnect: false,
// })Good for reference data and config blobs. Since nothing ever revalidates it, the only ways to refresh are an explicit mutate on the key or a full page load.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @tanstack/react-query | npm | You want devtools, first-class mutations, cache invalidation by predicate and offline persistence, and can accept the bigger API |
| @apollo/client | npm | Your backend is GraphQL and you want a normalized cache that updates related queries after a mutation |
| @reduxjs/toolkit | npm | You already run Redux and would rather have RTK Query generate hooks against the store you have than add a second cache |
| ky | npm | You only needed a nicer fetch with retries and timeouts, not a caching layer at all |