In SWR 2, how do null, function and array keys change what useSWR fetches, and what does the fetcher receive for an array key?
answer
- the key is also the fetcher input
- falsy means do not fetch
- a throwing key means not ready
- arrays arrive as one argument
- everything the fetcher uses
basics
~20 sA 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 sIn 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 linesimport 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
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.
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.
Diagnose wrong-user data from a token missing in the key, and request storms from keys holding Maps, Sets or dates created during render.
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.