Managing Server-Side Data Fetching in Nuxt 3 with useFetch and useAsyncData
Learn how to use useFetch and useAsyncData in Nuxt 3 to eliminate duplicate network requests during hydration and manage server-side state effectively.
08 Dec 2025, 02:13 UTC

The Hydration Mismatch and Duplicate Request Problem
When building Server-Side Rendered (SSR) applications, a common performance bottleneck is the "double fetch." This occurs when the server fetches data to render the initial HTML, but the client-side JavaScript executes the same request again upon hydration to establish its own state. This wastes server resources and causes visible layout shifts.
Nuxt 3 solves this by serializing the server-side response into a payload embedded in the HTML. When the client takes over, the data-fetching composables check this payload first. If the data exists, they use it immediately without making a network request. To implement this correctly, you must choose between useFetch and useAsyncData based on whether you are calling a URL or executing a custom logic block.
Choosing the Right Composable
While both composables prevent duplicate requests during hydration, they serve different architectural purposes.
useFetch: The URL Shortcut
useFetch is a high-level wrapper. It combines useAsyncData with $fetch. Use this when you have a specific endpoint (internal or external) and want the simplest syntax.
useAsyncData: The Logic Wrapper
useAsyncData is used when your data retrieval is more complex than a single URL call. Use this if you need to aggregate multiple API calls, perform database queries via a server route, or use a third-party SDK that doesn't rely on a simple fetch request.
| Feature | useFetch | useAsyncData |
|---|---|---|
| Primary Use Case | Direct API endpoints | Custom logic / Multiple sources |
| Implementation | Wrapper around useAsyncData | Primitive data-fetching tool |
| Key Generation | Automatic (based on URL) | Manual or automatic |
Implementation Guide
Scenario: Fetching Dynamic Product Data
In this example, we fetch product details based on a route parameter. We use a reactive URL to ensure that if the product ID changes, the data refreshes automatically.
<script setup>
const route = useRoute();
// We use a getter function for the URL to make it reactive.
// Nuxt will automatically re-fetch when route.params.id changes.
const { data: product, pending, error, refresh } = await useFetch(() => `/api/products/${route.params.id}`, {
key: `product-${route.params.id}`,
lazy: true,
server: true
});
</script>
<template>
<div v-if="pending">Loading product...</div>
<div v-else-if="error">Error loading product: {{ error.message }}</div>
<div v-else>
<h1>{{ product.name }}</h1>
<p>{{ product.description }}</p>
<button @click="refresh">Refresh Data</button>
</div>
</template>
Configuration Breakdown
- The Getter Function: Passing
() => urlinstead of a string makes the request reactive. If the ID changes, Nuxt triggers a new fetch. - The
keyOption: Nuxt generates keys automatically, but when using dynamic parameters, providing a unique key (e.g.,product-123) prevents different products from sharing the same cached state in the payload. lazy: true: This prevents the navigation from blocking. The page loads immediately, and thependingstate is used to show a loader while the data arrives.server: true: Ensures the request happens on the server during the initial page load.
Handling Imperative Calls
A common mistake is using useFetch inside a function (like a button click handler). Composables must be called during the setup phase of a component.
For event-driven requests, use $fetch. Unlike useFetch, $fetch does not track state or prevent duplicate hydration requests; it is a standard promise-based HTTP client.
<script setup>
async function updateProfile() {
try {
// $fetch is used here because it is an imperative action, not a setup-phase fetch
await $fetch('/api/profile', {
method: 'POST',
body: { name: 'New Name' }
});
alert('Profile updated!');
} catch (e) {
console.error('Update failed', e);
}
}
</script>
Verification and Diagnostics
To ensure your data fetching is working as intended and not duplicating requests:
- Inspect the Network Tab: Refresh the page. You should see the initial HTML load, but you should not see a duplicate XHR/Fetch request for the data already rendered on the page.
- Check the Page Source: View the page source (Ctrl+U) and search for
window.__NUXT__. You should find the fetched data serialized in this JSON object, confirming the server-to-client state transfer. - Test Client Navigation: Navigate to the page via a
<NuxtLink>. Confirm that the request now fires from the browser and that thependingstate is briefly visible.
Limitations and Risks
- Security: Never call a third-party API that requires a private API key directly from
useFetchifserver: falseis set or if the client might trigger the call. Instead, create a Nitro server route inserver/api/to act as a proxy, keeping the secret key on the server. - Reactivity Loss: Avoid destructuring the returned object of
useFetchif you intend to pass those values into other composables without usingtoRefs, as this can break the reactive link to the underlying state.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.