Preventing Duplicate API Calls in Nuxt 3 with useAsyncData
Learn how to use the Nuxt 3 useAsyncData composable to eliminate duplicate API calls during hydration and optimize server-side data fetching.
18 Oct 2026, 04:45 UTC

The Problem: Double-Fetching on Hydration
A common issue in Nuxt 3 applications is the "double-fetch." This occurs when a page fetches data on the server to render the initial HTML, but then immediately triggers the same network request again once the Vue application hydrates on the client. This wastes server resources, increases API costs, and can cause visible UI flickering.
The solution is useAsyncData. This composable ensures that data fetched during the server-side render (SSR) is serialized and passed to the client, allowing the client to "hydrate" (populate) its state from the server's result rather than initiating a new request.
How useAsyncData Synchronizes State
useAsyncData takes a unique key and an asynchronous fetch function. When the server executes this function, it stores the result in a payload associated with that key. When the browser takes over, Nuxt checks if that key already exists in the payload; if it does, it uses the cached value instead of running the fetch function again.
Implementation Example: Fetching Product Details
In this example, we fetch product data based on a route parameter. We use $fetch, which is Nuxt's built-in fetch utility that handles internal API calls efficiently.
<script setup>
const route = useRoute();
// The first argument 'product-data' is the unique key used for hydration
const { data, pending, error, refresh } = await useAsyncData(
`product-${route.params.id}`,
async () => {
// This function runs on the server during initial load
// and on the client during client-side navigation
return await $fetch(`https://api.example.com/products/${route.params.id}`);
}
);
</script>
<template>
<div v-if="pending">Loading product...</div>
<div v-else-if="error">Error loading product: {{ error.message }}</div>
<div v-else>
<h1>{{ data.name }}</h1>
<p>{{ data.description }}</p>
<button @click="refresh">Update Data</button>
</div>
</template>
Key Configuration Details
- The Key: The first argument (
`product-${route.params.id}`) must be unique. If you use a static string like'product'for all products, Nuxt may serve the cached data from a previous product page during client-side navigation. - $fetch vs useFetch: While
useFetchis a shorthand that combinesuseAsyncDataand$fetch, usinguseAsyncDataexplicitly is necessary when you need to perform complex logic (like multiple API calls or data transformation) before returning the final value. - The refresh function: The returned
refreshmethod allows you to manually trigger a re-fetch on the client without reloading the page.
Limitations and Common Pitfalls
The Client-Side Navigation Exception
It is important to understand that useAsyncData prevents double-fetching on the initial page load. However, when a user navigates to a new page via <NuxtLink>, the code is executing entirely on the client. In this scenario, the fetch function will run because there is no server-side payload for the new destination page.
Reactive Dependencies
If your fetch function depends on a reactive variable (like a search query), the data will not automatically update when that variable changes unless you pass the variable into the watch option:
const query = ref('nuxt');
const { data } = await useAsyncData(
'search',
() => $fetch(`/api/search?q=${query.value}`),
{ watch: [query] }
);
Verifying the Result
To confirm that useAsyncData is working and not double-fetching during initial load:
- Open your browser's Developer Tools and go to the Network tab.
- Refresh the page (Hard Reload).
- Observe the network requests. You should not see the API request for the product data in the network log, because the data was fetched on the server and embedded in the HTML payload.
- Now, navigate to a different product via a link. You should see the network request occur, as this is a client-side transition.
Diagnostic Check
If you suspect the function is running in the wrong environment, add a server-side check inside the fetch function:
async () => {
console.log('Fetching on server:', process.server);
return await $fetch('/api/data');
}
Check your terminal/server logs for Fetching on server: true and your browser console for Fetching on server: false (or undefined).
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.