In Nuxt 4, a flight header and a flight timeline both call `useFetch` for the same flight: one request or two, and what do they share?
answer
- keys, not URLs
- call site in the auto-key
- one entry, shared refs
- counted consumers, purged at zero
basics
~20 sTwo, unless both pass the same explicit key, because useFetch auto-keys per call site. With a shared key, Nuxt 4 keeps one entry whose data, error and status refs both read; transform and default must match, and the entry is purged after its last consumer unmounts.
solid answer
~40 sThe key decides sharing, not the URL. `useFetch`'s auto-key includes the call site, so two components requesting `/api/flights/LH400` get two keys, two entries and two requests. Give both the same explicit `key` and Nuxt 4's singleton data layer gives them one entry: the same `data`, `error`, `status` and `pending` refs, so a refresh in one updates the other. The entry keeps the options it was created with, so the handler, `transform`, `pick`, `getCachedData`, `default` and `deep` must match, or Nuxt warns in development; `server`, `lazy`, `immediate`, `dedupe`, `watch` and `enabled` may differ. If both mount at once under the default `dedupe: 'cancel'`, the second call restarts the first request; `'defer'` makes it join. When the last consumer unmounts, the entry is purged by default. Wrapping the call in one composable keeps options identical.
code
ts · 11 lines// app/composables/useFlight.ts
import type { MaybeRefOrGetter } from 'vue'
export function useFlight (id: MaybeRefOrGetter<string>) {
return useFetch(() => `/api/flights/${toValue(id)}`, {
// One entry per flight, whichever component asks
key: () => `flight:${toValue(id)}`,
// A second consumer joins the in-flight request
dedupe: 'defer',
})
}go deeper
Recall that useFetch keys are per call site, so sharing data between components needs the same explicit key.
Explain that one key means one entry with shared data, error and status refs, and which options must match across callers.
Show the production traps: conflicting options behind one key, concurrent consumers restarting each other under dedupe: 'cancel', and data purged on unmount.
Decide when a shared key beats a store, and own the key naming convention so unrelated features cannot collide on one entry.
## Keys decide sharing Every `useFetch` and `useAsyncData` call reads and writes an **entry** identified by a string key. The key, not the URL, decides whether two calls share anything. - `useFetch` without `key` hashes the **call site**, the URL and the fetch options. Two different components requesting `/api/flights/LH400` have two call sites, so two keys, two entries and two requests. Several instances of one component share a call site, so the same URL shares an entry. - `useAsyncData` without a string key gets a key unique to its file and line. - An explicit `key` (a string, ref or getter) makes any number of calls, in any components, point at one entry. ## The singleton entry Since Nuxt 4, all calls with the same key share **one** entry. What is shared: - the `data`, `error`, `status` and `pending` refs, so a refresh from the timeline updates the header; - the in-flight request and its `AbortController`; - the options the entry was **created** with, including the handler, `transform`, `pick`, `default`, `deep` and `getCachedData`; - the payload slot `nuxtApp.payload.data[key]`, which is also what `useNuxtData(key)` reads. Nuxt counts consumers per entry: each call adds one, and each component scope that is disposed removes one. ## Options that must match | Must match (NUXT_E3004 warning in development) | May differ per call | |---|---| | handler function | `server` | | `transform` | `lazy` | | `pick` | `immediate` | | `getCachedData` | `dedupe` | | `default` | `watch` | | `deep` | `enabled` | The warning exists because only one set can win. The entry keeps its creator's options, so a second caller with a different `transform` silently receives the first caller's shape. The docs recommend wrapping a shared call in one composable, so every consumer passes identical options by construction. ## Concurrent consumers and `dedupe` Sharing an entry does not by itself mean one request. When two components mount in the same render and both start the initial fetch: 1. The first call creates the entry and starts request A. 2. The second call finds A in flight and applies its own `dedupe` setting. 3. With the default `'cancel'`, it aborts A and starts request B. Both components end up with B's result, and code awaiting A waits for B. 4. With `'defer'`, it returns A's promise and starts nothing. Nuxt's own test suite asserts this: three same-key calls with default options run the handler three times, and with `'defer'` once. The docs' shared-key example says only one request is made; with concurrent mounts, the 4.5.2 source makes that hold only under `'defer'`. On a hydrated first load there is no fetch at all, because the payload already holds the data. ## Lifecycle: reference counting and purge - When the last consumer unmounts, Nuxt aborts any in-flight request for the entry and marks it inactive. - With the default `experimental.purgeCachedData` and no custom `getCachedData`, it then clears the entry and its payload slot, so returning to the page fetches again. - An entry with a custom `getCachedData` is **not** purged; that function is now your cache and your responsibility. - `refreshNuxtData(key)` refetches every consumer of a key, and `clearNuxtData(key)` resets it to its `default`. ## A shared composable ```ts // app/composables/useFlight.ts import type { MaybeRefOrGetter } from 'vue' export function useFlight (id: MaybeRefOrGetter<string>) { return useFetch(() => `/api/flights/${toValue(id)}`, { key: () => `flight:${toValue(id)}`, dedupe: 'defer', }) } ``` The header and the timeline both call `useFlight` with the route's flight id: one key per flight, identical options by construction, and one request when they mount together. ## When to share and when not to A shared key is the right tool when several components render **the same server resource at the same time**, such as one flight's header, timeline and gate panel. It is the wrong tool in two cases: - **Different shapes of the same URL.** If the timeline needs raw events and the header needs a summary built by `transform`, give them different keys; one entry can only hold one shape. - **Long-lived client state.** An entry lives only while something uses it and is purged after that. State that must outlive the page, or that the user edits across screens, belongs in a store. Key names deserve a convention, such as `resource:id`, because a collision between unrelated features produces no error, only a development warning and a wrong shape in production.
- How do you read or update the shared flight data from a component that did not fetch it?`useNuxtData('flight:LH400')` returns a `data` ref bound to the same key, which suits optimistic updates: assign the new value, send the mutation with `$fetch`, and restore the old value on error. To refetch every consumer at once, call `refreshNuxtData('flight:LH400')`; `clearNuxtData` resets entries to their `default`.
- Two features both use the key `flight` and the console shows NUXT_E3004. What is going on?Same-key calls share one entry, so Nuxt compares each caller's handler, `transform`, `pick`, `getCachedData`, `default` and `deep` against the entry and reports incompatible options. The entry keeps its creator's options, so one feature silently receives the other's data shape. Give different data different keys, or route both through one composable.
- Why does returning to a flight page refetch even though the data loaded a minute ago?By default Nuxt 4 purges an entry when the last component using it unmounts (`experimental.purgeCachedData`), so the return visit starts empty and fetches again. Keeping it requires a custom `getCachedData` that returns stored data; entries with a custom `getCachedData` are not purged on unmount.
A shared key is a whiteboard in a team room: everyone in the room reads and writes the same board, the first person in decides its layout, and the board is wiped when the last person leaves. Two rooms on the same subject still have two boards.
saying these in an interview costs you the question
- Two components calling useFetch with the same URL always share one request.
- Same-key calls can use different transform functions and each gets its own shape.
- Shared keys keep their data in memory for the whole browser session.
- With default options, a second same-key consumer simply waits for the first request.
- useNuxtData is Nuxt's name for reading a Pinia store.