useAsyncData and $fetch
useFetch is handy when a page needs one API address. When data is put together from several requests or does not come over HTTP, use useAsyncData: it accepts any async function. Inside that function you usually call $fetch, Nuxt's function for HTTP requests.
$fetch
$fetch is a wrapper around JavaScript's fetch. It parses JSON itself, throws an error on a 4xx or 5xx response, and on the server calls its own /api/... routes directly, without a network request:
<!-- app/pages/index.vue -->
<script setup lang="ts">
async function showCount() {
const posts = await $fetch('/api/posts')
alert(`Posts in the blog: ${posts.length}`)
}
</script>
<template>
<section>
<button @click="showCount">How many posts?</button>
</section>
</template>After a click a dialog shows the number of posts. $fetch knows the response type too: posts.length is accepted without errors.
useAsyncData
useAsyncData does with any function what useFetch does with an address: it runs it on the server and passes the result to the browser.
<!-- app/pages/index.vue -->
<script setup lang="ts">
const { data: posts } = await useAsyncData('posts', () => $fetch('/api/posts'))
</script>
<template>
<section>
<h2>Home</h2>
<p>Posts in the blog: {{ posts?.length }}</p>
</section>
</template>The first argument is the key under which Nuxt stores the data and passes it to the browser. The second is the loading function. The home page shows a line with the number of posts. useFetch('/api/posts') is essentially a shorthand for this call.
Several requests at once
The post page needs the post itself and its comments. Load them in parallel:
<!-- app/pages/posts/[id].vue -->
<script setup lang="ts">
const route = useRoute()
const id = route.params.id
const { data } = await useAsyncData(`post-${id}`, async () => {
const [post, comments] = await Promise.all([
$fetch(`/api/posts/${id}`),
$fetch(`/api/posts/${id}/comments`)
])
return { post, comments }
})
</script>
<template>
<article v-if="data">
<h2>{{ data.post.title }}</h2>
<p>{{ data.post.text }}</p>
<h3>Comments: {{ data.comments.length }}</h3>
<p v-for="(comment, i) in data.comments" :key="i">
<b>{{ comment.author }}:</b> {{ comment.text }}
</p>
</article>
</template>Promise.all starts both requests at the same time. At /posts/1 you see the post and its comments.
What to choose
| Task | Tool |
|---|---|
| Data from one API address when the page opens | useFetch |
| Several requests, a database or custom logic when the page opens | useAsyncData with $fetch |
| A request on a user action: a button, a form submit | $fetch |
Common mistake
Calling $fetch right in <script setup>: const posts = await $fetch('/api/posts'). The page shows up, but the request runs twice, on the server while rendering and again in the browser during hydration, because $fetch does not pass its result to the browser. For page data wrap $fetch in useAsyncData or use useFetch.