mrkeyoor.com_
Thu 06 Aug 10:56 UTC
npmWeb Frontendupdated 06 Aug 2026

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.

Verdict

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.

API stability5/5The v2 line has been out since 2022 and useSWR's signature has not changed; 2.4 and 2.5 only added things (preload, unload, cacheData). Two surfaces are still marked experimental in the types, useSWRSubscription and the new cacheData option, so treat those as movable.
Docs4/5swr.vercel.app is organised by task (conditional fetching, pagination, mutation, subscription) with runnable examples, and the TypeScript definitions carry long doc comments. Middleware, the cache provider interface and the exact revalidation defaults are thinner than the basics, and newer options land in the release notes before the site.
Maintenance4/52.5.0 shipped 3 August 2026 with the repository pushed the same day, and it is maintained by Vercel employees rather than one volunteer. Cadence is uneven though: 2.4.0 in February 2026 was followed by nothing until June, and around 145 open issues sit on the tracker.
Ecosystem4/515M installs a week and 32k stars, and it is the default suggestion in a lot of Next.js material. The surrounding ecosystem is much smaller than TanStack Query's: no official devtools, and third-party middleware for things like cache persistence is sparse and mostly unmaintained.

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
Skip it if

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

PackageRegistryPick it when
@tanstack/react-querynpmYou want devtools, first-class mutations, cache invalidation by predicate and offline persistence, and can accept the bigger API
@apollo/clientnpmYour backend is GraphQL and you want a normalized cache that updates related queries after a mutation
@reduxjs/toolkitnpmYou already run Redux and would rather have RTK Query generate hooks against the store you have than add a second cache
kynpmYou only needed a nicer fetch with retries and timeouts, not a caching layer at all