Data Fetching and Rendering Modes

useFetch and useAsyncData, where data fetching runs, and universal vs SPA vs static rendering.

useFetch and useAsyncData

Nuxt provides composables purpose-built for data fetching that are aware of server-side rendering, unlike calling a plain fetch/$fetch directly inside a component:

HTML
<script setup lang="ts">
const { data: posts, pending, error } = await useFetch('/api/posts');
</script>

<template>
  <p v-if="pending">Loading...</p>
  <p v-else-if="error">Failed to load posts.</p>
  <ul v-else>
    <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
  </ul>
</template>

useFetch(url) is really a thin, convenient wrapper around the more general useAsyncData, useful when you need to fetch from a URL. Reach for useAsyncData directly when the async logic is more than "call this one URL" — combining multiple calls, or wrapping a non-fetch async operation:

HTML
<script setup lang="ts">
const { data } = await useAsyncData('dashboard', async () => {
  const [user, orders] = await Promise.all([
    $fetch('/api/user'),
    $fetch('/api/orders'),
  ]);
  return { user, orders };
});
</script>

The first argument to useAsyncData is a key Nuxt uses to dedupe and cache the result — two components requesting the same key share one fetch instead of triggering it twice.

Where data fetching actually runs

During server-side rendering, the function passed to useFetch/useAsyncData runs on the server. Its result is then serialized into the initial HTML payload sent to the browser, and when the client-side app "hydrates" that HTML, it reuses the already-fetched data instead of calling the endpoint a second time. This is exactly what a raw fetch call inside a component's setup() would not do correctly on its own — it would run once on the server and then run again on the client during hydration, doubling the request and briefly showing two different results.

Rendering modes

Mode How it works Set via
Universal rendering (default) The server renders full HTML per request, then the client hydrates it into an interactive app Default — no config needed
SPA mode No server rendering at all; an (almost) empty HTML shell ships, and Vue renders everything client-side ssr: false in nuxt.config.ts
Static generation Every route is pre-rendered to a static HTML file at build time, then hosted like plain static files nuxt generate (or nuxt build --prerender)

Universal rendering is the right default for nearly everything public-facing — it gives fast first paint and works with SEO crawlers. SPA mode suits internal tools where SEO and initial load time matter less (an admin dashboard behind a login). Static generation suits content that's the same for every visitor and doesn't need a live server at all (documentation, marketing pages).

Common mistakes

  • Calling $fetch directly inside a component instead of useFetch/useAsyncData — it runs on the server during SSR, then runs again on the client during hydration, doubling the request and risking a flash of mismatched content.
  • Reusing the same useAsyncData key for two genuinely different requests in different components — Nuxt treats a matching key as "the same data" and can return one component's cached result to the other.
  • Setting ssr: false for a marketing or content page that actually needs to be indexed well by search engines — SPA mode ships a nearly empty initial HTML, which is a poor fit for SEO.