Using Nuxt 3's useAsyncData to Fetch Data Once and Avoid Hydration Mismatches
Learn how Nuxt 3's useAsyncData eliminates duplicate fetches and hydration mismatches by fetching data once on the server and reusing it on the client.
14 Dec 2025, 17:52 UTC

Problem: duplicate fetches and hydration mismatches
When a Nuxt page loads data directly in setup() or a composable, the request often runs twice: once on the server during rendering and again on the client after hydration. This leads to extra network traffic, slower time‑to‑interactive, and, if the server and client responses differ, a hydration mismatch warning in the console.
Thesis: useAsyncData centralises fetching and guarantees a single source of truth
The useAsyncData composable ties a data fetch to the route lifecycle. Nuxt executes the function on the server, serialises the result into the HTML payload, and reuses that payload on the client, eliminating the duplicate request and ensuring the hydrated tree matches the server‑rendered markup.
Worked example: a posts page
Create a page pages/posts.vue that fetches a list of posts from an internal API.
<template>
<div>
<h1>Latest posts</h1>
<ul v-if="posts">
<li v-for="post in posts" :key="post.id">
<strong>{{ post.title }}</strong>
<p>{{ post.excerpt }}</p>
</li>
</ul>
<p v-else-if="error">Error: {{ error.message }}</p>
<p v-else>Loading…</p>
</div>
</template>
<script setup lang="ts">
import { useAsyncData } from '#app'
import { $fetch } from 'ofetch'
const { data: posts, error, pending } = await useAsyncData(
'posts',
() => $fetch('/api/posts'),
{
// optional: revalidate when the route query changes
// watch: ['$route.query.page']
}
)
// expose to template
const postsRef = posts
const errorRef = error
const pendingRef = pending
</script>
Trade‑off: route‑level coupling and caching considerations
When to reconsider useAsyncData
- Because the fetch is bound to the route, you cannot call the same async function from multiple components without triggering another server request unless you share the result via a state store or a custom key.
- For data that changes very frequently (e.g., real‑time charts), the default caching may serve stale content. You can control freshness with the
refreshKeyoption or by manually callingawait useAsyncData(...).refresh(). - If you are using
nuxt generate(static site generation), ensure the API endpoint is reachable at build time; otherwise the fallback will be a client‑side fetch, which defeats the SSR benefit.
Verification steps
- Run
npm run devand open the posts page. In the browser’s Network tab, confirm that only one request to/api/postsappears on the initial load. - Right‑click the page, choose “View page source”, and search for the post titles. They should be present in the raw HTML, indicating server‑side rendering.
- Build for production:
npm run build && npm run start. Then runcurl http://localhost:3000/postsand verify that the returned HTML contains the post data without executing JavaScript. - Disable JavaScript in the browser (or use an extension like “Quick JavaScript Switcher”) and reload the page. The posts should still be visible, confirming that the content originated from the server.
Actionable closing
Use useAsyncData for any data that is needed to render a route. Pair it with a meaningful key (the first argument) so Nuxt can deduplicate calls across components. When you need to refresh the data, change the key or use the refreshKey option. Finally, always verify hydration by inspecting the raw HTML and the network tab to ensure the fetch truly happens only once.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.