skip to content

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%

answer

  1. the key is also the fetcher input
  2. falsy means do not fetch
  3. a throwing key means not ready
  4. arrays arrive as one argument
  5. everything the fetcher uses

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.

solid answer

~50 s

In SWR the key is both the cache identity and the fetcher's input. `null`, any other falsy key, or an empty array means 'do not fetch': `data` stays undefined and `isLoading` stays false, which is how conditional fetching works. A function key is called on each render. If it throws, as `() => '/api/projects?uid=' + user.id` does while `user` is undefined, or returns a falsy value, SWR treats the dependency as not ready, so a dependent request starts as soon as its input arrives. Array and plain-object keys get a stable hash, so a new array with the same contents is the same entry. In SWR 2 the fetcher receives the key as a single argument: `useSWR(['/api/user', token], ([url, token]) => ...)`. Before 2.0, array keys were spread into separate arguments. Anything the fetcher uses, such as a token, must be in the key, or different users share one entry.

code

tsx · 32 lines
tsx
import useSWR from 'swr'

type User = { id: string; name: string }
type Project = { id: string; title: string }

const getJson = async <T,>(url: string, token?: string): Promise<T> => {
  const res = await fetch(url, token ? { headers: { Authorization: `Bearer ${token}` } } : undefined)
  if (!res.ok) throw new Error(`Request failed: ${res.status}`)
  return res.json()
}

export function Projects({ token }: { token: string | null }) {
  // Conditional: no token, no request.
  const { data: user } = useSWR(token ? ['/api/user', token] : null, ([url, t]) =>
    getJson<User>(url, t),
  )

  // Dependent: the function throws while user is undefined, so SWR waits.
  const { data: projects } = useSWR(
    () => ['/api/projects?uid=' + user!.id, token],
    ([url, t]) => getJson<Project[]>(url, t ?? undefined),
  )

  if (!projects) return <p>Loading projects…</p>
  return (
    <ul>
      {projects.map((p) => (
        <li key={p.id}>{p.title}</li>
      ))}
    </ul>
  )
}

go deeper

for a junior

Remember that passing null as the key skips the request, and that everything the fetcher needs, like an id or token, belongs in the key.

for a middle

Explain function keys that throw to express dependencies, and that SWR 2 hands the fetcher the array key as one argument, unlike 1.x, which spread it.

for a senior

Diagnose wrong-user data from a token missing in the key, and request storms from keys holding Maps, Sets or dates created during render.

for a principal

Standardize key construction across teams, with one helper per resource that builds keys from plain values, so that cache identity cannot silently omit credentials or parameters.

## The key is identity and input In SWR the first argument to `useSWR` is the **key**. It decides **which cache entry** the hook reads, and it is **passed to the fetcher**. By default `useSWR('/api/user', fetcher)` calls `fetcher('/api/user')`. Because the same value does both jobs, changing its shape changes both what is cached and what the fetcher receives. ## Four key shapes | Key | Request? | Fetcher receives | Typical use | |---|---|---|---| | a string, `'/api/user'` | yes | the string | a simple resource | | `null`, `''`, `false` or `[]` | no | nothing | conditional fetching | | a function, `() => …` | only if it returns a truthy key without throwing | the returned key | dependent fetching | | an array or plain object, `['/api/orders', id]` | yes | the whole array or object, as one argument | parameters, credentials | ## Conditional and dependent fetching - **Conditional:** `useSWR(shouldFetch ? '/api/data' : null, fetcher)`. While the key is `null` the hook sends nothing, and `data` is `undefined` with `isLoading` false. - **Dependent:** `useSWR(() => '/api/projects?uid=' + user.id, fetcher)`. While `user` is still `undefined`, reading `user.id` **throws**. SWR catches that, treats the key as not ready and waits. When the user query resolves, the component re-renders, the function returns a real key and the projects request starts. - The docs describe the dependent pattern as keeping as much parallelism as possible, while making a request wait only when it genuinely needs data from another. ## Array keys and the v2 change Array keys carry several inputs at once. SWR 2 passes the **key itself** to the fetcher, so the fetcher destructures it: ```ts useSWR(['/api/user', token], ([url, token]) => fetchWithToken(url, token)) ``` Versions before 2.0 **spread** an array key into separate fetcher arguments, `(url, token)`. Code copied from older examples often breaks on exactly this point. Plain-object keys have worked since 1.1.0, when SWR started serializing object-like keys, and the fetcher receives the object. ## The missing-token bug The docs call this pattern incorrect, and the reason is worth being able to explain: 1. A hook is written as `useSWR('/api/user', (url) => fetchWithToken(url, token))`. 2. The key is only `'/api/user'`, so the token plays no part in the entry's identity. 3. After the token changes, for example when a different user signs in, the key is the same, so SWR serves the old entry and nothing forces a request with the new token. 4. The fix is to put every input the fetcher depends on into the key: `useSWR(['/api/user', token], …)`. ## Choosing a shape - Use a **plain string** for a simple GET whose URL already identifies the data. - Use an **array** when the fetcher needs more than the URL, such as a token, a locale or a filter object, so that every input is part of the identity. - Use `null` or a **throwing function** rather than an `enabled` flag; SWR has no such option, because the key itself carries the condition. - Build keys in a **custom hook** per resource, so every component that reads the resource spells the key the same way. ## How keys are hashed - Array and plain-object keys go through SWR's **stable hash**. Arrays are hashed element by element and plain objects with **sorted** property names, skipping `undefined` values. A new array with the same contents on every render is therefore the same key. - Other objects, such as a `Map`, a `Set` or a function, are hashed by **identity**. A new instance created during render is a new key on every render, so pass plain values instead. - A `Date` is hashed through its ISO string, so `new Date()` created during render also changes the key every time. - `useSWRInfinite` applies the same rules page by page: its `getKey(pageIndex, previousPageData)` returns a key for each page, and returning `null` means there are no more pages to load. - `unstable_serialize(key)` from `'swr'` returns the string SWR would use, which is handy when a test or a server needs the same cache key.

  • Why does SWR catch an error thrown by a function key instead of reporting it?
    Throwing is the documented signal that a dependency is not ready yet, typically because it reads a property of data that has not loaded. SWR treats the key as empty, sends nothing and tries again on the next render. That makes dependent fetching a one-liner without explicit null checks.
  • You pass { url: '/api/orders', filters } as the key and see a new request on every render. What would you check?
    Whether something inside the object is recreated on each render and hashed by identity, such as a `Set` or `Map` of filters, or is time-based, such as `new Date()`. Plain objects and arrays of primitives hash by value, so replacing those values with plain data stabilizes the key.

saying these in an interview costs you the question

  • A null key makes useSWR throw or return an error.
  • In SWR 2 an array key is spread into separate fetcher arguments.
  • Values the fetcher closes over do not need to be in the key.
  • A new array literal on every render causes a new request each time.
  • Dependent fetching in SWR needs a useEffect that waits for the first request.