useFetch and useAsyncData
Nuxt's data-fetching composables, and why they behave differently from calling fetch() inside onMounted.
読了時間 2 分
In a plain Vue component, fetching data usually means calling fetch() inside onMounted and storing the result in a ref. That pattern has a real problem in Nuxt: onMounted only runs in the browser, so during server-side rendering the page would render with no data at all, then re-fetch once the client takes over — a wasted request and a flash of empty content. useFetch and useAsyncData solve this by fetching during SSR and making the result available to the client without fetching twice.
useFetch for calling an API
<script setup>
const { data: posts, pending, error, refresh } = await useFetch("/api/posts");
</script>
<template>
<p v-if="pending">Loading…</p>
<p v-else-if="error">Failed to load posts.</p>
<ul v-else>
<li v-for="post in posts" :key="post.id">{{ post.title }}</li>
</ul>
</template>useFetch runs on the server during SSR, serializes the result into the initial HTML payload, and reuses that same data on the client instead of re-requesting it — this is often called "hydration" of fetched data. data, pending, and error are all reactive refs you can use directly in the template.
useAsyncData for anything else
useFetch(url) is really a thin wrapper around the more general useAsyncData, which accepts any async function — useful when you're calling a third-party SDK, combining multiple requests, or doing anything beyond a single HTTP call:
<script setup>
const { data: dashboard } = await useAsyncData("dashboard", async () => {
const [user, stats] = await Promise.all([
$fetch("/api/user"),
$fetch("/api/stats"),
]);
return { user, stats };
});
</script>The first argument, "dashboard", is a cache key. Nuxt uses it to avoid duplicate fetches for the same data across a page and to match up server- and client-side results during hydration — pick a key unique to what's being fetched, not a generic one shared across unrelated calls.
Why not just call $fetch directly?
$fetch (Nuxt's built-in fetch helper, itself auto-imported) works fine for one-off requests, like inside a submit handler. The reason useFetch/useAsyncData exist alongside it is the SSR-hydration behavior above, plus the reactive pending/error/refresh state they hand back for free:
<script setup>
async function handleDelete(id) {
await $fetch(`/api/posts/${id}`, { method: "DELETE" });
await refresh(); // re-run the useFetch call above
}
</script>As a rule of thumb: reach for useFetch/useAsyncData for data a page needs to render, and $fetch for one-off calls triggered by user actions, like submitting a form or deleting a row.