skip to content

SWR

SWR is the lightweight stale-while-revalidate hook: return cached data immediately, revalidate in the background, and expose mutate for local updates. It is often compared with TanStack Query in interviews, so know where it is deliberately smaller.

on this pageshow

questions

5

With SWR, a profile header and a settings page both call useSWR('/api/user', fetcher); how many requests go out, and how do they share data?

level: juniorimportance: must knowfreq 48%

answer

  1. the key names one cache entry
  2. both hooks subscribe to it
  3. a two-second dedupe window
  4. cached data first, then revalidate
  5. isLoading versus isValidating

basics

~20 s

Both hooks share the one cache entry named by '/api/user'. Mounted together they send one request, because SWR dedupes revalidations of a key within dedupingInterval (2000 ms). A later mount shows the cached user at once and revalidates in the background.

solid answer

~40 s

In SWR the key is the cache identity, so both components read and subscribe to the same entry in the cache provider. When they mount together, the first hook starts the request and the second sees it already in flight and does not start another. SWR also keeps that request registered for `dedupingInterval` (2000 ms by default) after it resolves, so revalidations of the key inside that window reuse it. Both hooks get the same `data` and re-render together. If the settings page mounts later, outside the window, it renders the cached user immediately with `isLoading` false, and revalidates in the background with `isValidating` true, because `revalidateIfStale` defaults to `true`. Anything that updates the key, such as a `mutate` after the user saves, updates both components at once.

code

tsx · 30 lines
tsx
import useSWR from 'swr'

type User = { id: string; name: string; avatarUrl: string }

const fetcher = async (url: string): Promise<User> => {
  const res = await fetch(url)
  if (!res.ok) throw new Error('Failed to load user')
  return res.json()
}

export function useUser() {
  return useSWR('/api/user', fetcher)
}

export function HeaderAvatar() {
  const { data: user, isLoading } = useUser()
  if (isLoading) return <span className="avatar-skeleton" />
  return <img src={user?.avatarUrl} alt={user?.name ?? ''} />
}

export function SettingsPage() {
  const { data: user, isLoading, isValidating } = useUser()
  if (isLoading) return <p>Loading settings…</p>
  return (
    <section>
      <h1>Settings for {user?.name}</h1>
      {isValidating ? <small>Refreshing…</small> : null}
    </section>
  )
}

go deeper

for a junior

Know that the key identifies one shared cache entry, that components using the same key share data and requests, and that isLoading means there is no data yet.

for a middle

Explain deduplication with dedupingInterval, why a later mount shows cached data and revalidates because revalidateIfStale is true, and how isLoading differs from isValidating.

for a senior

Spot the production traps: near-identical key strings, one key served by two fetchers, and treating dedupingInterval as a freshness window. Centralize keys in custom hooks.

for a principal

Decide how the app names and owns keys, with one hook per resource, so that request volume and cache consistency stay predictable as teams add components.

## One key, one cache entry `useSWR(key, fetcher)` is SWR's data hook. The **key**, here the string `'/api/user'`, does two jobs. It is the **identity of a cache entry**, and it is the **argument passed to the fetcher**. Every hook in the app that uses the same key, under the same cache provider, reads the same entry and **subscribes** to it. When the entry changes, every subscribed component re-renders with the new value. There is no per-component copy of the data. ## Mounting together: deduplication On the settings route, the header and the settings page mount in the same commit. SWR handles them like this: 1. The first hook finds no cached data for `'/api/user'`, so it starts a revalidation, calls the fetcher and records the request for that key. 2. The second hook mounts, sees a request for the key already registered, and skips its own mount revalidation. 3. The response arrives and is written once to the cache entry. Both hooks are subscribed, so both re-render with the user. 4. SWR leaves the request registered for **`dedupingInterval`**, **2000 ms** by default, after it resolves. A mount, focus or reconnect revalidation for the same key inside that window reuses the finished request instead of sending a new one. The result is **one** request, however many components ask for the key at once. ## Mounting later: cached data, then revalidation If the user opens the settings page a minute after the header loaded, the window has long passed: - The settings page renders **immediately with the cached user**. That is the stale-while-revalidate behaviour SWR is named after. - Because **`revalidateIfStale`** defaults to `true`, it also starts a background revalidation on mount. - When the new response arrives, the entry updates and **both** the header and the settings page re-render, even though only one of them triggered the request. ## isLoading versus isValidating SWR exposes two loading flags, and the difference matters here: | Situation | `data` | `isLoading` | `isValidating` | |---|---|---|---| | First mount, nothing cached, request running | `undefined` | `true` | `true` | | Later mount, cached user shown, revalidating | cached user | `false` | `true` | | Settled, nothing running | user | `false` | `false` | | Key is `null`, so nothing is fetched | `undefined` | `false` | `false` | Use `isLoading` for skeletons, because it is true only while there is no loaded data. Use `isValidating` for a subtle "refreshing" indicator, if you show one at all. ## What updates both components - A **bound `mutate`** from either hook, or the **global `mutate('/api/user', …)`** from `useSWRConfig()`, writes the entry, and every subscriber re-renders. - **Focus** and **reconnect** revalidations run per mounted hook, but they are deduplicated per key just like mount revalidations. - **Polling** with `refreshInterval` is off by default (`0`). ## Options are per hook, data is per key Each `useSWR` call carries its **own options**, but the **entry is shared**. That combination surprises people: - If the header uses `useSWRImmutable('/api/user', fetcher)` and the settings page uses plain `useSWR`, the settings page still revalidates on mount, and the header re-renders with the result, even though the header itself never revalidates. - A `refreshInterval` on one hook polls the shared entry, so every component on that key sees the polled data. - `fallbackData` on one hook is only that hook's initial value; it is not written to the shared entry. When the settings page needs fresher data than the header, give the settings page's hook the stricter options. The header benefits automatically. ## Pitfalls - `'/api/user'` and `'/api/user/'` are **different keys**, so they give two entries and two requests. - Two hooks with the same key but **different fetchers** share one entry. The hook that happens to start a request uses its own fetcher, and both hooks then show that result. One key should always mean one fetcher and one response shape. - `dedupingInterval` collapses **repeats**; it does not keep data fresh for a period. A mount outside the window revalidates again unless you turn `revalidateIfStale` off for that hook.

  • The header mounts, and the settings page mounts 1.5 seconds after the first response arrived. Is a second request sent?
    No. SWR keeps the finished request registered for `dedupingInterval` (2000 ms by default) after it resolves, and a mount inside that window does not revalidate again. The settings page renders the cached user straight away. At 2.5 seconds it would have started a background revalidation instead, because `revalidateIfStale` is true by default.
  • Why wrap useSWR('/api/user', fetcher) in a custom useUser hook?
    It keeps the key and the fetcher in one place, so every component uses exactly the same key string and the same response shape. A typo such as a trailing slash would otherwise create a second entry and a second request. It also gives you one place to add options or types later.

saying these in an interview costs you the question

  • Each useSWR call sends its own request, so two components mean two requests.
  • Each component keeps its own copy of the data it fetched.
  • isLoading is true whenever a revalidation is in progress.
  • Deduplication only works when both components pass the same fetcher function.
  • A later mount waits for the network before showing anything.
open as a page

In SWR 2, how do null, function and array keys change what useSWR fetches, and what does the fetcher receive for an array key?

level: middleimportance: should knowfreq 40%

basics

~20 s

A null or falsy key fetches nothing. A function key is called, and fetches nothing if it throws or returns a falsy value. Array and object keys are hashed stably and reach the fetcher as one argument, not spread.

open as a page

In SWR, how do you use mutate to show a user's new display name in the header immediately and roll it back if saving fails?

level: middleimportance: should knowfreq 42%

basics

~20 s

Call mutate for '/api/user' with the save promise and optimisticData set to the edited user. The header updates at once; if the promise rejects, rollbackOnError (default true) restores the previous data, and revalidate (default true) refetches afterwards.

open as a page

Compared with TanStack Query, what does SWR deliberately leave out of its cache model, and how does that change the code you write?

level: middleimportance: should knowfreq 38%

basics

~20 s

SWR has no freshness setting like staleTime, no automatic eviction like gcTime, and no AbortSignal for the fetcher. You turn revalidation off per hook where it is not needed, clear the cache yourself, and handle cancellation in your own code.

open as a page

After one user logs out and another logs in, an SWR app briefly shows the previous user's profile and orders; why, and how do you clear SWR's cache correctly?

level: seniorimportance: should knowfreq 30%

basics

~20 s

SWR's cache outlives the session, and '/api/user' is the same key for both users, so the new user first sees the old cached data. On logout call unload({ revalidate: false }) (SWR 2.5+), or give each session its own cache provider.

open as a page