Confused about when to use $fetch, useFetch, or useAsyncData in Nuxt? This guide breaks down their differences, best use cases, and common pitfalls to help you make the right choice for your project.
Get notified when we release new tutorials, lessons, and other expert Nuxt content.
"Should I use $fetch
, useFetch
, or useAsyncData
here?"
If you've built anything with Nuxt, you've probably asked yourself this question.
These three data-fetching tools might seem similar at first glance, but choosing the wrong one can lead to performance issues, unnecessary API calls, DX weirdness, or SSR headaches.
In this guide, I will demystify when and why to use each option, complete with some practical examples and common pitfalls to avoid. You'll learn:
First off, we’ll quickly take a look at their differences.
We have three approaches to retrieving data in Nuxt:
$fetch
— A low-level HTTP client that runs on both the server and the client, powered by ofetch
.useAsyncData
— An SSR composable for more advanced operations and transformations.useFetch
— A simple wrapper over useAsyncData
and $fetch
to simplify the most common use cases.These are similar but aim to solve different scenarios. Here’s a quick table:
Let me share with you some examples.
Tool | Key Use Case | SSR Handling | Typical Scenario |
---|---|---|---|
$fetch | Direct HTTP client (server or client) | Server: Skips a real HTTP round trip for internal Nuxt endpoints. Client: Normal network call, improved with interceptors and auto retries. | Server routes, or client-side event-driven actions |
useAsyncData | Complex SSR tasks (multi-fetch, transformations) | Fetches once on the server, then hydrates to the client, so no double-fetching | Any async method can be used with this composable, so the opportunities are endless |
useFetch | Combines $fetch and useAsyncData together | Uses useAsyncData so we get the same benefits | Fetching data from a URL, such as a product list or blog posts page |
Here are a few scenarios to help illustrate when and why we might want to use each of these data fetching methods.
The useFetch
composable is the best choice to load initial data once on the server. It prevents extra calls after hydration.
Suppose we want to display a list of blog posts:
<script setup>
const { data: posts } = await useFetch('/api/posts')
</script>
<template>
<div>
<h1>My Posts</h1>
<div v-for="post in posts" :key="post.id">{{ post.title }}</div>
</div>
</template>
Here, we're fetching the data once on the server, then the client hydrates the data from the Nuxt payload, preventing extra calls.
Using useFetch
instead of useAsyncData
just provides a simpler interface. Here’s how it would look if we used useAsyncData
instead:
<script setup>
const { data: posts } = await useAsyncData(() => {
return $fetch('/api/posts');
})
</script>
<template>
<div>
<h1>My Posts</h1>
<div v-for="post in posts" :key="post.id">{{ post.title }}</div>
</div>
</template>
The $fetch
method is more direct for forms or user-triggered actions. It doesn’t store SSR data in the Nuxt payload and doesn’t automatically prevent re-fetching.
But on the client, it’s great for anything triggered by a user event, such as button clicks or searching, or anything like that. Here, we’re creating a new contact following a form submission:
async function submitForm() {
if (!formData.value) return
const response = await $fetch('/api/contact', {
method: 'POST',
body: formData.value,
});
formData.value = null;
}
It's great for event-driven actions like form submissions or button clicks or anything like that.
The useAsyncData
composable lets us use whatever async operation we want and have it work with SSR. Because of this, it also gives us concurrency control or custom transformations, or really anything you can stick in an async function.
For example, we can fetch multiple endpoints in parallel and then transform the results:
<script setup>
const { data, error } = await useAsyncData('dashboard', async () => {
const [user, stats, notifications] = await Promise.all([
$fetch('/api/user'),
$fetch('/api/stats'),
$fetch('/api/notifications')
])
return { user, stats, notifications }
})
</script>
<template>
<div>
<h1>Dashboard</h1>
<p v-if="error">Error loading data</p>
<div v-else>
<p>{{ data.user.name }}</p>
<p>{{ data.stats.totalLogins }}</p>
<p>{{ data.notifications.length }} new notifications</p>
</div>
</div>
</template>
We can't do this with useFetch
because it only works with a single endpoint, using $fetch
under the hood.
The $fetch
method is great for server routes, server middleware, or plugins.
On the server, it avoids extra network overhead when calling Nuxt’s internal endpoints. This speeds up server calls:
export default defineEventHandler(async (event) => {
// Calls an internal endpoint directly
// Instead of making an HTTP request, it calls the
// event handler like any other function
const user = await $fetch('/api/user', {
headers: event.req.headers
});
// Do something with user
});
We skip an HTTP request for /api/user
because Nuxt calls the route function in the same process.
Instead of making a real HTTP request, it just calls the route function directly.
We can also use $fetch
for calling external APIs, but for those we’ll always have to make a full HTTP request like you’d expect.
Now let’s go over a few different common pitfalls and gotchas that can often trip up Nuxt devs (including myself).
If we call $fetch
in a server-rendered component, Nuxt doesn’t push that data to the client. The client will have to call $fetch
again on hydration in order to get the data.
This is no good!
We can switch to useFetch
or useAsyncData
to avoid that.
useAsyncData
We can accidentally reuse a key, or not provide a key at all.
This is actually pretty easy to forget to do, and I've done it myself many times!
Although useAsyncData
will try to automatically generate a unique key for us, it's best to provide one yourself. This way, you can be sure that the key is unique.
By default, useFetch
and useAsyncData
block navigation until their requests are done.
If we prefer to load data after navigation, we should pass lazy: true
or use useLazyFetch
/ useLazyAsyncData
.
The $fetch
method will throw an error if the response is not OK.
For useFetch
or useAsyncData
, we rely on error
and status
to handle errors gracefully, letting us show a UI error message.
Understanding when to use $fetch
, useFetch
, or useAsyncData
is important for building efficient Nuxt applications.
Here's a simple decision framework:
useFetch
for simple, SSR-friendly data fetching from a single endpointuseAsyncData
when you need something more than fetching from a single URL$fetch
for client-side actions or server-side callsEach tool has its strengths, and using them appropriately will help you build faster, more maintainable applications.
Remember that SSR data handling is the key consideration — useFetch
and useAsyncData
are designed to prevent double-fetching, while $fetch
is perfect for direct HTTP requests.
If you’d like to dive deeper into data fetching with Nuxt, Vue School has an in-depth course on this very topic. It covers many of the topics relayed in this article and more with hands on examples and demonstrations.