Keys and Serialization
Purpose and Scope
In SWR, a key is more than a label for a request. It is the value that ties a React hook invocation to a cache entry, an in-flight fetch, preloaded data, mutation state, and revalidation subscribers. The public hook accepts a key and a fetcher; internally, SWR needs a normalized string cache key so that independent components, revalidation events, and cache updates can coordinate around the same resource. This page explains how keys are interpreted, how serialization makes complex keys usable as cache identifiers, and what behavior the repository tests lock in when keys change during rendering.
The important reader problem is avoiding accidental data reuse. If a component switches from one resource key to another, SWR must not show a completed response from the old key as if it belonged to the new key. It also must be able to represent disabled or conditional requests. The tests in this repository treat those details as correctness boundaries: empty and null inputs serialize to no cache key, string inputs stay readable, structured keys hash deterministically, and a key transition resets visible data until the new request resolves unless another option supplies replacement data.
Sources: test/unit/serialize.test.ts, test/use-swr-key.test.tsx, src/_internal/types.ts
Relevant Source Files
- test/unit/serialize.test.ts — Verifies
unstable_serializeoutput for empty arrays,null, string keys, and nested array/object arguments by comparing structured keys withstableHash. - test/use-swr-key.test.tsx — Exercises runtime hook behavior when keys change, including stale responses from earlier keys, undefined data during transitions, synchronous fetchers, and function keys.
- src/_internal/types.ts — Defines the internal global state maps keyed by cache key strings and the public fetcher typing that derives fetcher arguments from
Key, including disabled falsy keys.
Core Concepts
A serialized key is the cache-facing form of a user-supplied key. The unit test for unstable_serialize demonstrates three important categories. Empty array input and null both serialize to an empty string, which SWR treats as no active cache identity for a request. A plain string key serializes to itself, preserving the common URL-like case. A structured array containing numbers, objects, and nested arrays serializes through stableHash, giving SWR a deterministic string for compound request identity without requiring the application to manually join values.
The structured-key case is especially important for applications that need to fetch by multiple inputs, such as an endpoint plus parameters, authentication state, locale, or a pagination cursor. Rather than encouraging ad hoc string concatenation, SWR can accept a richer argument and convert it into a stable cache key. The test does not merely assert that a hash exists; it asserts equality with stableHash for a nested array/object shape. That couples public serialization behavior to the same stable hashing mechanism used internally for complex key identity.
Conditional fetching is represented by falsy key results. The TypeScript Fetcher type in src/_internal/types.ts makes this visible at the type level: when an SWR key is a function returning a value, the fetcher receives that returned value; when the key is null, undefined, or false, the fetcher type becomes never, reflecting that the fetcher should not be called for a disabled request. This complements the serialization test where null maps to an empty serialized key, and it gives both runtime and type-system signals for conditional request flows.
Sources: test/unit/serialize.test.ts, src/_internal/types.ts
Runtime Behavior When Keys Change
Key changes are treated as resource changes, not as simple rerenders. One hook test starts with a long request for an initial key, then switches to a new key whose request finishes sooner. SWR renders the short request result and keeps that value even after the earlier long request eventually completes. The test then forces another rerender and verifies that the old response still does not replace the current key's data. This protects components from race conditions where a slow response for an obsolete resource would otherwise overwrite a newer resource.
Another key-change test documents the visible transition state. A component first fetches with a key ending in 0, receives that response, then later changes to a key ending in 1. During the gap between the new key being selected and the new fetch completing, the rendered data is expected to be undefined. The comment in the test explicitly marks this intermediate state as required because showing the previous key's response at that moment would misrepresent which resource the component is rendering.
The same principle applies even when the fetcher is synchronous. In the synchronous fetcher test, a click increments a sample key from 1 to 2. The component first rerenders with the new key and no data, then receives the synchronous result for that new key. That sequence matters because SWR's contract is based on key identity, not only promise timing. A fetcher that can immediately compute a value still should not let the previous key's cached result leak into the next render as if it belonged to the new request.
Function keys add another dimension because their return value can depend on closure state. The key test suite includes a case named for revalidation when a function key changes identity. The component stores an id, creates a closure-based function key for that id, and uses a fetcher that resolves the key value. This validates the pattern where a key can be computed lazily while still participating in SWR's dependency tracking: changing the function identity or its closed-over result must be considered when deciding whether to revalidate.
Sources: test/use-swr-key.test.tsx
Type and Cache Mapping
The type definitions show why normalized string keys are central to SWR's internals. GlobalState is a tuple of maps and functions used by the cache provider. Its event revalidators map string cache keys to revalidation callbacks. Mutation timestamps map string cache keys to [start_timestamp, end_timestamp] tuples. The fetch cache maps string cache keys to [data, timestamp] tuples, and the preload cache maps string cache keys to fetcher responses. Cache setting and subscription functions also accept a string key, making serialization the boundary between public key shapes and internal storage.
This mapping explains why stable serialization must be deterministic. If two equivalent structured keys serialized differently, SWR would split fetch state, mutation metadata, and subscribers across separate cache entries. If two different keys serialized to the same string, unrelated resources could share data or revalidation events incorrectly. The tests therefore focus on the observable pieces of that boundary: empty and disabled inputs do not form normal request keys, strings remain stable as direct identifiers, and arrays or objects flow through the stable hash path used for complex identities.
Fetcher typing reinforces the same design from the API side. BareFetcher accepts any arguments and can return data or a promise, but Fetcher<Data, SWRKey> is constrained by the key type. If the key is a function returning an argument, the fetcher receives the resolved argument. If the key is a falsy disabled value, no fetcher call is valid. Otherwise, the fetcher receives the key itself. That means application code can choose simple string keys, compound array/object keys, or computed keys while still getting type inference aligned with the actual fetcher input.
Sources: src/_internal/types.ts
Compact Reference
| Key form | Serialization or fetch behavior | Source-backed signal |
|---|---|---|
null | Serializes to an empty string and represents no active request identity. | unstable_serialize(null) expectation. |
[] | Serializes to an empty string. | unstable_serialize([]) expectation. |
'key' | Serializes to the same string. | unstable_serialize('key') expectation. |
[1, { foo: 2, bar: 1 }, ['a', 'b', 'c']] | Serializes with stableHash. | Unit test compares unstable_serialize with stableHash. |
| Function key | Fetcher receives the function's returned value when active. | Fetcher conditional type and function-key hook test. |
null, undefined, or false key type | Fetcher is typed as never, matching disabled fetching. | Fetcher conditional type. |
| Changed key | Current data becomes undefined until the new key resolves, unless other configuration supplies data. | Runtime key-change hook tests. |
| Old in-flight response | Must not overwrite the visible data for a newer key. | Long-request versus short-request hook test. |
A practical rule follows from the reference: choose keys that describe the resource, not the component. If two components should share the same fetched result and revalidation lifecycle, give them equivalent keys. If they represent different resources or different parameter sets, encode those differences in the key. For simple APIs, a URL string is enough. For parameterized APIs, prefer structured keys and let SWR serialize them rather than manually building fragile strings.
Implementation Guidance
When building a custom hook on top of SWR, expose the domain inputs and construct the SWR key in one place. For example, a user hook can use a string key when the user id is known and return null when it is not. That conditional key communicates both runtime intent and fetcher typing: no id means no active request, no fetcher call, and no normal cache entry for that missing resource. Once the id exists, the serialized key becomes the coordination point for data, errors, mutations, and revalidation.
For compound resources, use a stable structure whose values fully describe the request. The serialization test covers arrays containing nested objects and arrays, so a key can model multiple request dimensions without losing determinism. The fetcher should accept the same value shape that the key represents, because the Fetcher type is designed to infer arguments from the key. This keeps the cache identity and network request inputs aligned, which is the safest way to avoid fetching one resource while caching the result under another.
Finally, be deliberate about UI expectations during transitions. The hook tests show that after a key changes, SWR prefers an undefined data state over displaying data from the previous key. If the desired product behavior is to keep old data visible while the new key loads, use the appropriate SWR option or pattern for previous or fallback data rather than assuming key changes preserve it. For deeper follow-up, read the pages on Fetchers and Data Flow, Cache and Providers, and Loading, Error, and Previous Data States.