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:
<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:
<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
$fetchdirectly inside a component instead ofuseFetch/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
useAsyncDatakey 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: falsefor 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.