A bird.

Max Levytskyi

Frontend Developer

Nuxt 3: useAsyncData vs useFetch vs $fetch

A comparison and explanation of how useAsyncData, useFetch, and $fetch work in Nuxt 3, with guidance on choosing the right approach for handling asynchronous data and requests.

Nuxt 3: useAsyncData vs useFetch vs $fetch

Data fetching is an essential part of any modern web application. Complex web applications often require not only retrieving data from the server and displaying it on the page, but also handling loading and error states, caching, and other processes. With server-side rendering (SSR), things become even more complicated: we must consider that data retrieval can occur either on the server or on the client in the browser.

Hopefully, modern frameworks such as Nuxt.js provide us with tools to make data fetching easier and more predictable. Nuxt 3 offers different APIs for data fetching: useAsyncData, useFetch, and $fetch, which are flexible and powerful functions. However, with great power comes great responsibility, so it’s important to understand the differences between them and determine which function to use in different scenarios.

Let’s figure it out!

$fetch as the foundation for requests

Let’s imagine we have only $fetch function and we’re gonna to fetch a user.

app.vue
<template>
    <h1>Hello, {{ data.userName }}!</h1>
</template>

<script setup lang="ts">
const { data } = await $fetch(`/api/user`)
</script>

Every time Nuxt encounters a call to the $fetch function, it will make an HTTP request. Nuxt 3 uses the ofetch library under the hood. Essentially, the only function of $fetch is to make requests.

It’s worth noting that during server-side rendering, all internal server requests will lead to the direct invocation of the corresponding functions. This means that requests to /api will not result in a new HTTP request. Instead, Nuxt 3 will directly call the handler function, such as defineEventHandler, emulating the request itself. This approach significantly improves rendering speed and frees up resources for additional requests.

Problems arise when we deal with Server-Side Rendering. Remember, while using Universal Rendering (the default rendering mode in Nuxt 3), our code executes twice: once on the server and once on the client side during hydration. This means $fetch will also be called twice—once on the server and then in the browser—resulting in two requests. This is inefficient, as instead of making a single request to retrieve data, we end up making two, which slows down the process and can lead to issues.

Additionally, after receiving data on the server, it can change quickly. Calling $fetch in the browser will retrieve new, updated data, which may differ from the data received on the server and lead to a problem called hydration mismatch: we must ensure that the initial rendering is consistent on both the server and the browser.

Equally important, $fetch doesn’t store the data it retrieves. It simply hands the data over, saying, “Here’s the data, do whatever you want with it.” This means that after receiving data from $fetch, you are responsible for caching, storing, and passing the data to the client.

We’ll explore the specific use cases for $fetch a bit later. But for now, let’s get acquainted with a new tool: useAsyncData.

useAsyncData enters the game

To make working with asynchronous data convenient and easy, we need to do the following things:

  1. We need to uniquely identify the data we receive so that we can securely pass it to the client and reuse it to maintain consistent behavior within the server-client environment of our application.
  2. We want to choose how we retrieve our data, whether through Nuxt’s server layer, third-party services, or other methods.
  3. We want to leverage the power and flexibility of Vue’s SSR, routing, and reactivity systems without writing additional code.

The useAsyncData composable meets all these requirements. It accepts 3 parameters:

If you are familiar with Nuxt’s useState composable, you can think of useAsyncData as similar to useState, but designed for handling asynchronous data with all the related functionality.

Let’s consider this code:

app.vue
<template>
  <div>
    <span v-for="item of data">
      {{ item.name }}
    </span>
  </div>
</template>

<script setup lang="ts">
  const { data, status, error, refresh, clear } = await useAsyncData('cats',
  () => {
    return $fetch('/api/cats')
  })
</script>

When Nuxt performs a request on the server side, it saves the data under the key cats in a special internal payload object, which is then serialized and sent to the browser when rendering is complete.

When Nuxt starts executing code in the browser, it checks for the presence of data under the key cats and uses it instead of making a new request. Note how this approach differs from a simple $fetch call. Nuxt aims to unify server-client data handling and reuses data wherever possible.

In pseudocode, it would look something like this.

nuxt/src/app/composables/asyncData.ts
// ⚠️ pseudo code
function getUseAsyncData(key: string, handler: () => Promise<unknown>) {
    // check if we are on the client side
    // and the data is already in the payload
    if (import.meta.client && payload.data[key]) {
        return payload.data[key]
    } else {
        // if not, make a request
        return await handler()
    }
}

This is a highly simplified representation of how the code works. You can view the complete code for useAsyncData in the Nuxt Github repository.

useAsyncData returns the following data:

These aren’t all the features of useAsyncData. In addition to the key and data processing function, we can pass a third parameter: an options object that makes working with useAsyncData even more convenient and adaptable to your specific needs.

server option

The name of this option speaks for itself. If we pass false, Nuxt will not execute the request on the server side; instead, the request will be executed on the client side. This can be useful if the data is not critical for SEO and you want to speed up server-side rendering.

It’s worth noting that this approach can lead to the known issue of Cumulative Layout Shift (CLS), an important Web Vitals metric for visual stability, which can significantly impact user experience.

The default value is true.

lazy option

This parameter applies to client-side navigation and allows you to specify whether you want to wait for the request to resolve before navigating to the page or if you prefer to execute the request after transitioning to the page.

Using this option can improve navigation speed; however, it may result in the user seeing loading indicators or skeleton components.

It’s important to note that this option applies only to client-side navigation. On the server side, Nuxt will always wait for the data to resolve. If you want to disable the request on the server side, use the server option instead.

This option is so popular that Nuxt created special composables, useLazyAsyncData and useLazyFetch, which automatically enable this option.

The default value is false.

immediate option

As the name suggests, this option determines whether we want to resolve the data immediately or wait for a manual call or a change in reactive data to trigger the request. To avoid leaving the data empty, you can use another option, default, and pass a function that returns default data.

This option can be useful in cases where we want to delay the request.

The default value is true.

transform option

This option allows you to modify the data returned by the request. It is useful when the data needs to be adjusted based on the component’s state or other factors, or if you cannot change the format of the data returned by the server.

pick option

To understand this option, it’s important to remember that Nuxt transfers resolved server-side data to the browser in a special internal payload object. This means that all data retrieved from the useAsyncData function will be serialized and embedded in the HTML document, which the browser will load along with other files and layout elements.

But what if the data is too large, and you’re only using a subset of it? In this case, you can specify the keys you need, and Nuxt will automatically remove any unnecessary data.

Use this option to speed up server-side rendering or page loading.

app.vue
<template>
    <h1>{{ data.name }}</h1>
    <p>{{ data.description }}</p>
</template>

<script setup lang="ts">
const { data } = await useAsyncData('cats', () => {
    return $fetch('/api/large/document')
}, {
    pick: ['name', 'description']
})
</script>

watch option

When working with Vue, we often need to make requests in response to changes in specific data. To make useAsyncData work more smoothly with Vue’s reactivity system, you can use the watch option. This enables Nuxt to automatically refresh the data in response to changes in the specified properties.

deep option

By default, Nuxt uses ref to return data, making it deeply reactive. You can set this option to false, and Nuxt will use shallowRef instead, thereby disabling deep reactivity and improving performance.

This option can be useful when working with large data arrays.

dedupe option

By default, when useAsyncData makes a new request, it automatically cancels the previous request and initiates a new one. You can change this behavior by setting the dedupe option to defer. In this case, Nuxt will reuse the existing request and wait for it to resolve.

The default value is cancel.

What about useFetch?

Up until now, we haven’t discussed useFetch, which is one of the most popular methods for making requests. This is because $fetch is the primary and most commonly used method for making requests in Nuxt 3, while useAsyncData provides convenient data handling in a server-client architecture.

To avoid writing $fetch inside useAsyncData every time, the developers introduced useFetch to help reduce repetitive code. Instead of a key, it accepts a URL and uses the same options as useAsyncData, along with additional options from the ofetch library (essentially $fetch).

Let’s take a look at using useFetch:

app.vue
<script setup lang="ts">
const { data } = await useFetch('/api/cats', {
  params: {
    limit: 10
  }
})
</script>

This code is essentially a simplification of the following equivalent:

app.vue
<script setup lang="ts">
const { data } = await useAsyncData('/api/cats', () => {
    return $fetch('/api/cats', {
        params: {
          limit: 10
        }
    })
})
</script>

As you can see, useFetch is simply a streamlined version of useAsyncData and $fetch, which helps avoid code duplication and provides a more declarative approach to data fetching.

In addition to the options from useAsyncData, useFetch also allows you to pass additional options, such as:

What to Use?

Given the variety of tools for working with asynchronous data in Nuxt, it’s important to choose the right approach for specific situations:

Conclusion

Nuxt 3 offers multiple approaches for managing requests, giving developers the flexibility to work in a declarative style or take full control over the request process. Understanding these options is essential for building efficient and scalable applications.

A bird.

Thanks for reading! Let's stay in touch.

— Max Levytskyi
Frontend Developer
Follow @immaxdev