Back

Server-Side Data Fetching in Nuxt

Server-Side Data Fetching in Nuxt

If you’re building a Nuxt application and wondering why your data fetches twice—or why components sharing the same key behave unexpectedly—you’re not alone. Nuxt SSR data fetching has specific rules that trip up even experienced developers.

This article explains how useAsyncData and useFetch work in Nuxt 4, covering payload hydration, navigation behavior, key management, and the pitfalls that cause the most confusion.

Key Takeaways

  • Nuxt runs useFetch and useAsyncData on the server, serializes responses into the HTML payload, and hydrates on the client without refetching
  • Components sharing the same key share identical reactive state—use distinct keys for independent data instances
  • In current Nuxt 4 releases, certain options (handler, transform, pick, getCachedData, default, deep) must match across calls sharing a key
  • Use useFetch or useAsyncData for SSR-safe fetching; reserve $fetch for event handlers and client-only code

How Nuxt Executes Server-Side Data Fetching

When you call useFetch or useAsyncData in a page or component, Nuxt runs that fetch on the server during the initial request. The server serializes the response into a payload embedded in the HTML. When the client hydrates, it reads this payload instead of refetching—eliminating duplicate network requests.

const { data } = await useFetch('/api/products')

This single line executes on the server, embeds the result in the page, and hydrates seamlessly on the client. No double-fetching. No hydration mismatch.

Blocking vs. Lazy Fetching

By default, Nuxt blocks navigation until awaited data fetching completes. This ensures your page renders with data already available.

For client-side navigation, you can opt into lazy fetching:

const { data, status } = useLazyFetch('/api/comments')

With lazy fetching, navigation does not block by default while the fetch runs in the background. You’ll need to handle the loading state in your template using the status ref.

Understanding Nuxt Data Fetching Keys

Keys are central to how Nuxt caches and deduplicates requests. Every useFetch call uses the URL as its default key. For useAsyncData, you provide the key explicitly or let Nuxt generate a deterministic one for you.

Here’s the critical part: components sharing the same key share the same state. This includes data, error, status, and pending refs.

// Component A
const { data } = await useAsyncData('users', () => $fetch('/api/users'))

// Component B - shares state with Component A
const { data } = await useAsyncData('users', () => $fetch('/api/users'))

Both components receive identical reactive refs. Change one, and the other reflects it.

Key Consistency Rules

In current Nuxt 4 versions, Nuxt enforces consistency for certain options when multiple calls share a key. These must match:

  • Handler function
  • transform function
  • pick array
  • getCachedData function
  • default value
  • deep option

These can differ safely:

  • server
  • lazy
  • immediate
  • dedupe
  • watch

Violating consistency triggers development warnings and unpredictable behavior.

Safe Key Strategies

For route-specific data, include route parameters in your key:

const route = useRoute()
const { data } = await useAsyncData(
  `product-${route.params.id}`,
  () => $fetch(`/api/products/${route.params.id}`)
)

For independent instances that shouldn’t share state, use distinct keys:

const { data: sidebar } = await useAsyncData('users-sidebar', fetchUsers)
const { data: main } = await useAsyncData('users-main', fetchUsers)

Nuxt Data Caching and Dedupe Behavior

Nuxt automatically deduplicates concurrent requests with matching keys. If three components request the same key simultaneously, only one network request fires.

The dedupe option controls refresh behavior:

const { data, refresh } = await useFetch('/api/data', {
  dedupe: 'cancel' // cancels pending requests before starting new one
})

In Nuxt 4.2 and later, cancellation support is significantly improved. When supported, stale responses from previous routes can be cancelled or ignored during rapid navigation, reducing the risk of outdated data briefly appearing.

More details: https://nuxt.com/docs/api/composables/use-fetch

Common Pitfalls

Confusing Nuxt’s useFetch with Other Libraries

Nuxt’s useFetch is not the same as @vueuse/core’s useFetch or similar utilities. Nuxt’s version handles SSR payload hydration automatically. Using a different library’s useFetch bypasses this entirely, causing double-fetching and hydration mismatches.

Using $fetch in Setup Without useAsyncData

Calling $fetch directly in <script setup> runs on both server and client:

// ❌ Fetches twice
const data = await $fetch('/api/users')

// ✅ Fetches once, hydrates correctly
const { data } = await useFetch('/api/users')

Reserve $fetch for event handlers and client-only interactions.

Reusing Keys with Conflicting Options

This triggers warnings and bugs:

// ❌ Conflicting deep options
await useAsyncData('users', fetchUsers, { deep: false })
await useAsyncData('users', fetchUsers, { deep: true })

Expecting Data Before Hydration with server: false

When you set server: false, data remains null until hydration completes—even if you await the composable.

Conclusion

Nuxt 4’s data fetching model centers on server execution, payload hydration, and key-based caching. Keep keys stable and unique per data source. Ensure options consistency when sharing keys across components. Use useFetch or useAsyncData for SSR-safe fetching, and reserve $fetch for client-side interactions.

Master these patterns, and you’ll avoid the double-fetching and state-sharing bugs that frustrate most Nuxt developers.

FAQs

This typically happens when you use $fetch directly in script setup instead of useFetch or useAsyncData. Direct $fetch calls run on both server and client. Wrap your fetches in useFetch or useAsyncData to leverage payload hydration, which fetches once on the server and reuses that data on the client.

Use useFetch when fetching from a URL directly—it handles keys automatically based on the URL. Use useAsyncData when you need custom logic, multiple API calls combined, or explicit control over the cache key. Both provide the same SSR hydration benefits.

Components sharing the same key share identical reactive state. To keep data independent, use unique keys for each component. For example, use users-sidebar and users-main instead of just users when two components fetch the same endpoint but need separate state.

The dedupe option controls how Nuxt handles multiple refresh calls. Setting dedupe to cancel aborts any pending request before starting a new one. This helps avoid race conditions during rapid user interactions and ensures newer responses take precedence when cancellation is supported.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay