In Nuxt 4, why should a page load its data with `useFetch` or `useAsyncData` instead of calling `$fetch` in `<script setup>`, and when is `$fetch` right?
answer
- setup runs in two places
- what the HTML carries along
- the Nuxt payload, keyed
- user actions after the page is live
basics
~20 suseFetch and useAsyncData fetch during server rendering and ship the result in the Nuxt payload, so hydration reuses it instead of refetching. A bare $fetch in setup runs on the server and again in the browser; keep $fetch for user-triggered calls.
solid answer
~50 sOn a first visit a Nuxt page's setup runs twice: on the server to render HTML, then in the browser to hydrate it. A bare `$fetch('/api/flights/LH400')` in `<script setup>` therefore hits the API twice, and if the second answer differs the page can flicker or mismatch during hydration. `useFetch`, a wrapper around `useAsyncData` and `$fetch`, stores the server result in the payload under a key, and the browser reads it from there while hydrating. On the server it also forwards the visitor's cookies and headers for relative URLs through `useRequestFetch`, and on client navigation it holds the route until the data arrives unless you pass `lazy: true`. `$fetch` is right for things the user triggers once the page is live, such as a POST on submit, and inside a `useAsyncData` handler. Reach for `useAsyncData` when the source is not one URL: an SDK, or several `$fetch` calls combined.
code
vue · 24 lines<script setup lang="ts">
const route = useRoute()
// Server-rendered; the browser reuses the result from the payload
const { data: flight, status } = await useFetch(
() => `/api/flights/${route.params.id}`,
)
// User-triggered, browser only: plain $fetch
async function notifyMe () {
await $fetch('/api/alerts', {
method: 'POST',
body: { flightId: route.params.id },
})
}
</script>
<template>
<p v-if="status === 'error'">Flight not found</p>
<section v-else-if="flight">
<h1>{{ flight.number }}: {{ flight.state }}</h1>
<button @click="notifyMe">Notify me</button>
</section>
</template>go deeper
Recall that setup runs on the server and again in the browser, and that useFetch hands the server result over in the payload while a bare $fetch does not.
Explain the keyed payload handoff, why useFetch forwards cookies for relative URLs on the server, and why useAsyncData wraps sources that are not one URL.
Show you can spot double fetches in review: bare $fetch in setup, handlers returning undefined, useFetch called after mount, or VueUse's useFetch imported by mistake.
Frame the team rule: composables for render-time data, $fetch for user actions, and a shared wrapper built with createUseFetch so the conventions hold across many pages.
## Why setup code runs twice A Nuxt page is **universal** by default. On the first visit the server runs each component's `<script setup>` to render HTML, and the browser runs the same code again to **hydrate** that HTML into a live Vue app. Anything in setup with a side effect happens in both places, and a network call is a side effect: 1. The server runs `await $fetch('/api/flights/LH400')`, renders the flight into HTML and sends it. 2. The browser loads the JavaScript, runs setup again and calls `$fetch` a second time. 3. The API is hit twice per page view, time to interactive grows, and if the second answer differs from the first (a flight that just moved from *boarding* to *departed*), the page can flicker or mismatch during hydration. ## What `useFetch` and `useAsyncData` add Both composables wrap the request in a **keyed entry** that Nuxt serialises into the **payload**, the object embedded in the HTML and exposed as `useNuxtApp().payload`. - On the server they run the request and store the result under the key. - In the browser, during hydration, they find that key in the payload and skip the request. - On client-side navigation they run the request in the browser and, unless you pass `lazy: true`, hold the navigation until it resolves (Nuxt uses Vue's `<Suspense>` for this). - They return refs, `data`, `error`, `status` (`'idle' | 'pending' | 'success' | 'error'`) and `pending`, plus the functions `refresh`/`execute` and `clear`. - On the server, for a relative URL, `useFetch` uses `useRequestFetch`, so the visitor's cookies and headers reach your own API; a bare server-side `$fetch` does not forward them. `useFetch(url)` is essentially `useAsyncData(key, () => $fetch(url))` with a key derived from the call site, the URL and the fetch options. `useAsyncData` is the general form: it takes any async **handler** and, since Nuxt 4.2, passes it `{ signal }` so cancellation can reach the network. ## Comparing the three | | `$fetch` | `useFetch` | `useAsyncData` | |---|---|---|---| | What it is | the ofetch client, auto-imported | composable over `useAsyncData` + `$fetch` | composable over any async handler | | Server result reaches the browser | no | yes, via the payload | yes, via the payload | | Key | none | auto (call site, URL, options) or `key` | first argument, or auto per call site | | Blocks client navigation | only if you await it | yes, unless `lazy` | yes, unless `lazy` | | Where to call it | anywhere | component setup, Nuxt plugins | component setup, Nuxt plugins | ## When `$fetch` is the right tool 1. **User-triggered requests** once the page is interactive: a POST on submit, a *Notify me* button, a delete. Nothing needs to be server-rendered or handed over. 2. **Inside a `useAsyncData` handler**, where the composable already provides the key and the payload handoff, for example `Promise.all` over two `$fetch` calls. 3. **Server code**, such as one Nitro handler calling another. The composables are for data a component needs in order to render. Calling one after the component has mounted, for example in `onMounted`, makes Nuxt report **NUXT_E3003** in development, with the advice to use `$fetch` for requests triggered after mount. ## Traps worth naming in an interview - **A handler that returns `undefined`.** Hydration skips the request only when the payload holds a value for the key, so a handler that returns nothing runs again in the browser. Nuxt reports **NUXT_E3006** in development and suggests returning `null` instead. - **The wrong import.** `import { useFetch } from '@vueuse/core'` shadows Nuxt's auto-import. VueUse's `useFetch` has no payload handoff, and the Nuxt docs point to this import when `data` unexpectedly comes back as a string. - **Reserved names.** The compiler transforms `useFetch` and `useAsyncData` calls, so do not name your own wrapper `useFetch`; Nuxt 4.4 added `createUseFetch` and `createUseAsyncData` for variants with custom defaults. - **Side effects in the handler.** The handler should be side-effect free. For one-time work, such as seeding state once per request, the docs point to `callOnce`. The short version for an interview: composables for data the page renders, `$fetch` for what the user does, and one keyed entry that crosses from server to browser so the request happens once.
- What does `useAsyncData` give you that `useFetch` does not?Control over the handler. `useFetch(url)` is roughly `useAsyncData(key, () => $fetch(url))` with a key built from the URL, the options and the call site. `useAsyncData` accepts any async function, such as an SDK client or `Promise.all` over several `$fetch` calls, and you choose the key yourself. Since Nuxt 4.2 the handler also receives `{ signal }`, which you pass to `$fetch` so cancellation reaches the network.
- Why does Nuxt warn when a `useAsyncData` handler returns nothing?During hydration the browser skips the request only if the payload holds a value for the key, and `undefined` counts as no value. A handler that returns nothing therefore looks unfetched and runs again in the browser, which is the double fetch the composable exists to prevent. In development Nuxt reports this on the server as NUXT_E3006 and suggests returning `null`, which is a real value.
- What does Nuxt report if `useFetch` runs after the component has mounted, for example in `onMounted`?In development it reports NUXT_E3003: the call came after mount, so the fetch cannot be awaited during setup. The suggested fix is to use `$fetch` for requests triggered after mount, such as in event handlers, or to move the `useFetch` call back into setup where it belongs.
saying these in an interview costs you the question
- useFetch and $fetch are the same thing; useFetch is just shorter to write.
- A bare $fetch in setup is fine because Nuxt caches every request automatically.
- useFetch only runs in the browser, like a mounted hook.
- Call useFetch inside click handlers so the POST result gets cached.
- Importing useFetch from @vueuse/core in a Nuxt page gives the same payload handoff.