skip to content

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%

answer

  1. no freshness window
  2. no automatic eviction
  3. no cancellation signal
  4. switches per hook instead
  5. a smaller API in exchange

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.

solid answer

~50 s

Three omissions change daily code. Freshness: TanStack Query's `staleTime` marks data fresh for a period, and a fresh entry is not refetched on mount or focus. SWR has no such setting. With cached data a mount still revalidates (`revalidateIfStale: true`), focus revalidation is only throttled by `focusThrottleInterval` (5000 ms), and only `dedupingInterval` (2000 ms) collapses repeats. So for rarely-changing data you switch revalidation off per hook or use `useSWRImmutable`. Eviction: TanStack Query garbage-collects unused entries after `gcTime` (five minutes by default); SWR's provider is a `Map` that keeps entries until you remove them with `unload()` or `mutate`, or swap the provider. Cancellation: TanStack Query gives every `queryFn` an `AbortSignal`; SWR's fetcher receives only the key, and a superseded response is discarded, not aborted. In exchange, SWR's surface is small: the key is the fetcher's input and one `mutate` covers updates.

code

tsx · 25 lines
tsx
import useSWR from 'swr'
import useSWRImmutable from 'swr/immutable'

type Country = { code: string; name: string }
type Hit = { id: string; title: string }

const getJson = <T,>(url: string): Promise<T> =>
  fetch(url).then((r) => {
    if (!r.ok) throw new Error(`Request failed: ${r.status}`)
    return r.json() as Promise<T>
  })

// Rarely changes: opt out of every automatic revalidation.
export function useCountries() {
  return useSWRImmutable<Country[]>('/api/countries', getJson)
}

// Search: debounce outside, and let SWR discard superseded responses.
export function useSearch(debouncedTerm: string) {
  return useSWR<Hit[]>(
    debouncedTerm ? `/api/search?q=${encodeURIComponent(debouncedTerm)}` : null,
    getJson,
    { keepPreviousData: true },
  )
}

go deeper

for a junior

Know that SWR and TanStack Query solve the same problem, and that SWR is smaller: fewer settings, one key concept and one mutate function.

for a middle

Name the three concrete omissions, freshness window, automatic eviction and cancellation signal, and the SWR tools that stand in for each: revalidation switches, unload and a hand-made AbortController.

for a senior

Explain the operational effects: extra revalidation traffic without a freshness window, cache growth on long sessions, and wasted server work from uncancelled requests, and how you would mitigate each in SWR.

for a principal

Frame the choice as how much cache policy the team wants built in versus written by hand, and whether the app's data patterns need that policy at all.

## Two libraries, one idea SWR and TanStack Query both implement **stale-while-revalidate** for server data in React: show cached data at once, fetch in the background, and share one cache entry per key. They differ in how much policy they build in around that idea. SWR is deliberately smaller. The differences below are the ones that change the code you write, not a list of features. ## Difference 1: no freshness window | Behaviour | TanStack Query v5 | SWR 2 | |---|---|---| | "Do not refetch while data is recent" | `staleTime` per query; a fresh entry is not refetched on mount or focus | no equivalent setting | | Mount with cached data | refetches only if the entry is stale (`staleTime` defaults to `0`) | revalidates, because `revalidateIfStale` defaults to `true` | | Window focus | refetches stale entries | revalidates, throttled by `focusThrottleInterval` (5000 ms) | | Collapsing bursts | concurrent fetches of a key are shared | `dedupingInterval` (2000 ms) reuses a recent request | In SWR, `dedupingInterval` is the only time-based brake, and it is meant for collapsing duplicates, not for expressing freshness. For data that rarely changes, such as a feature-flag list or a country table, SWR code switches revalidation **off per hook**: - `revalidateIfStale: false`, `revalidateOnFocus: false` and `revalidateOnReconnect: false`, or - **`useSWRImmutable`** from `'swr/immutable'`, which the docs describe as equivalent to those three switches. ## Difference 2: no automatic eviction TanStack Query removes an **unused** entry after `gcTime`, five minutes by default. SWR's default cache provider is a `Map` that keeps entries **until you remove them**. That changes three habits: 1. **Logout** must clear the cache explicitly, with `unload()` (SWR 2.5+) or `mutate(() => true, undefined, { revalidate: false })`. 2. **Long-lived pages** that visit many distinct keys, such as a search box keyed by the query string, accumulate entries for the life of the page. 3. **Bounding memory** is your job. A cache provider is any object with `get`, `set`, `delete` and `keys`, so a size-limited map that drops its oldest keys can stand in for the default `Map` when a page really does visit thousands of keys. 4. **Scoping** is done with providers: `<SWRConfig value={{ provider: () => new Map() }}>` gives a subtree its own cache, which is also how tests isolate state. ## Difference 3: no cancellation signal TanStack Query passes an **`AbortSignal`** in the query function's context and aborts it when a query becomes out of date or inactive, if the function uses it. SWR calls the fetcher with **only the key**. When two requests for a key overlap, SWR keeps the later one and **discards** the earlier result, calling `onDiscarded(key)`, but the earlier request still runs to completion. If wasted requests matter, for example for expensive search requests, you handle it yourself: - **debounce** the input, so fewer keys, and therefore fewer requests, are produced in the first place; - if the server work itself must stop, manage an **`AbortController` in your own fetcher**, remembering that an aborted `fetch` rejects and SWR stores that rejection as the key's `error`, so the fetcher or the UI has to treat an abort differently from a real failure. ## What SWR offers instead - **One concept, the key**, which is both identity and fetcher input, including `null` for "do not fetch" and a throwing function for "not ready". - **One `mutate`**, bound or global, with `optimisticData`, `populateCache`, `rollbackOnError` and `revalidate`, plus a filter-function form that targets many keys at once. - **Global defaults** through `SWRConfig`, and middleware through the `use` option. ## How to talk about the choice A concrete framing works better than a verdict: - Choose SWR when most data is fine to revalidate often and the team values a tiny API. - Expect to write policy yourself, with per-hook revalidation switches, explicit cache clearing and hand-made cancellation, in the places where TanStack Query would give you `staleTime`, `gcTime` and `signal`. - Neither choice changes the underlying model. Only the amount of built-in policy differs.

  • Can you get something like staleTime in SWR by raising dedupingInterval?
    Partly. A request stays registered for `dedupingInterval` after it resolves, and mount, focus and reconnect revalidations inside that window reuse it, so a large value suppresses automatic refetches for that long. But `mutate` bypasses deduplication, and the setting describes request deduplication, not how long data stays valid. Explicit revalidation switches or `useSWRImmutable` state the intent more clearly.
  • What happens to the earlier response when two SWR requests for the same key overlap?
    SWR keeps the request that started later. When the earlier one resolves, its result is ignored and `onDiscarded(key)` is called. The earlier request is not aborted, because SWR's fetcher has no signal, so the network and server work still happens unless your fetcher cancels it itself.

saying these in an interview costs you the question

  • SWR's dedupingInterval is the same thing as TanStack Query's staleTime.
  • SWR garbage-collects unused cache entries after five minutes.
  • SWR passes an AbortSignal to the fetcher like TanStack Query does.
  • SWR cannot update more than one key with a single mutate call.
  • The two libraries use different caching models, not just different defaults.