Using Nuxt 3's useAsyncData for Efficient Server‑Side Data Fetching
Learn how Nuxt 3's useAsyncData composable fetches data once on the server, serializes it into HTML, and reuses it during client navigation to avoid duplicate requests and flicker.
18 Mar 2026, 21:22 UTC

Problem: Fetching data without duplicating requests
When building a Nuxt 3 page you often need data that is available on the server for SEO and initial paint, but you also want to avoid making the same request again when the user navigates client‑side. Writing separate fetch logic for server and client leads to extra network traffic, flashing loading states, and more code to maintain.
Thesis: useAsyncData solves this by running the async function once on the server, serializing the result into the HTML, and reusing it during client navigation.
How useAsyncData works
The composable accepts an async function, a unique key, and optional options. During server‑side rendering (SSR) Nuxt executes the function, awaits the promise, and injects the resolved value into the rendered HTML as a JSON script. On the client, the same key is read from that script, so no additional request is made for the same data while the page is hydrated. If the user navigates to another route that also uses the same key, Nuxt keeps the previous value visible while a background request refreshes the data, preventing flicker.
The returned object contains three reactive refs:
data– the resolved value (ornullwhile pending).pending–truewhile the async function is running.error– holds any thrown error ornullif successful.
These refs enable straightforward template handling with v-if/v-else blocks.
Worked example: fetching a list of posts
Create a page pages/posts.vue that displays posts from JSONPlaceholder.
<template>
<div>
<h1>Posts</h1>
<v-if>{{ pending }}</v-if>
<p>Loading…</p>
</v-if>
<v-else-if>{{ error }}</v-else-if>
<p class="error">Failed to load: {{ error.message }}</p>
</v-else-if>
<ul v-else>
<li v-for="post in data" :key="post.id">
<strong>{{ post.title }}</strong>
<p>{{ post.body }}</p>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { useAsyncData } from '#app'
const { data, pending, error } = await useAsyncData(
'posts',
() => fetch('https://jsonplaceholder.typicode.com/posts').then(r => r.json()),
{
// optional: customize serialization (default is JSON.stringify)
// serialize: (value) => JSON.stringify(value, null, 2)
}
)
</script>
When you run npx nuxi dev and view the page source, you will see a __NUXT__ script containing the posts array. Navigating away and back via <NuxtLink> will not trigger another network request for the same key; the pending flag will briefly flip to true while a background refresh occurs, but the UI retains the previous list.
Trade‑offs and limitations
While useAsyncData reduces duplicate requests, the serialized payload becomes part of the initial HTML. Large datasets (e.g., thousands of rows) can increase page size noticeably, affecting Time to First Byte. In such cases consider:
- Pagination or infinite scrolling to limit the amount fetched per request.
- Streaming the response on the server and sending chunks incrementally.
- Disabling SSR for that specific fetch by setting
server: falsein the options, which makes the data client‑only and keeps the initial HTML lightweight.
Error handling is important: any thrown error inside the async function is caught and exposed via the error ref. However, an unhandled promise rejection (e.g., forgetting to await a promise) will bubble up and can break SSR rendering. Wrapping the function body in a try/catch or ensuring all promises are awaited prevents this.
Actionable steps to verify behavior
- Create a fresh Nuxt 3 project:
npx nuxi init use-async-demo && cd use-async-demo. - Add the
pages/posts.vuefile shown above. - Start the dev server:
npx nuxi dev. - Open
http://localhost:3000/posts, view page source, and confirm the posts data appears inside the__NUXT__script. - Navigate to another page and back using
<NuxtLink>. In the DevTools Network tab, verify that no new request tojsonplaceholder.typicode.com/postsis made while thependingflag briefly turns true. - To test client‑only fetching, modify the options to
{ server: false }. Reload the page and observe that the initial HTML no longer contains the posts; they appear only after hydration.
These steps confirm the core mechanics of useAsyncData without relying on any external claims.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.