Javascript is required
·
7 min read

A Comprehensive Guide to Data Fetching in Nuxt 3

A Comprehensive Guide to Data Fetching in Nuxt 3 Image

With Nuxt 3's rendering modes, you can execute API calls and render pages both on the client and server, which has some challenges. For example, we want to avoid duplicate network calls, efficient caching, and ensure that the calls work across environments. To address these challenges, Nuxt provides a built-in data fetching library ($fetch) and two composable (useFetch and useAsyncData).

In this article, I'll explain everything you need to know about the different data fetching methods available in Nuxt 3 and when to use them.

Data Fetching Library

Nuxt has a built-in library for data fetching: ofetch

ofetch is built on top of the fetch API and provides some handy features like:

  • Works on node, browser, and workers.
  • Smartly parses JSON and native values in the response.
  • Automatically throw errors when response.ok is false with a friendly error message and compact stack (hiding internals).
  • Automatically retries the request if an error happens.
  • You can provide async interceptors to hook into lifecycle events of ofetch calls.

You can use ofetch in your whole application with the $fetch alias:

const todos = await $fetch('/api/todos').catch((error) => error.data)

useFetch

useFetch is the most straightforward way to handle data fetching in a component setup function.

Component.vue
1<script setup>
2const { data, error, pending, refresh } = await useFetch('/api/todos')
3</script>
4
5<template>
6  <span v-if="pending">Loading...</span>
7  <span v-else-if="data">Todos: {{ data }}</span>
8  <span v-else-if="error">Error: {{ error }}</span>
9  <button @click="refresh">Refresh</button>
10</template>

useFetch returns three reactive variables and a function:

  • data: a reactive variable that contains the result of the asynchronous function that is passed in.
  • error: a reactive error object containing information about the request error.
  • pending: a reactive boolean indicating whether the request is in progress.
  • refresh/execute: a function to refresh the data returned by the handler function. By default, Nuxt waits until a refresh is finished before it can be executed again.

The useFetch composable is responsible for forwarding the data to the client if the API call was executed on the server. This way, the client doesn't need to refetch the same data on the client side when the page hydrates. You can inspect this payload via useNuxtApp.payload(); the Nuxt DevTools visualize this data in the payload tab.

useFetch additionally reduces API calls by using a key to cache API responses. The key is automatically generated based on the URL and the fetch options. The useFetch composable is auto-imported and can be used in setup functions, lifecycle hooks, and plugin or route middleware.

You can use the value of a ref in the URL string to ensure your component updates when the reactive variable changes:

1const todoId = ref('uuid')
2
3const { data: tracks, pending, error } = useFetch(() => `/api/todos/${todoId.value}`)

If the todoId value changes, the URL will update accordingly and the data will be fetched again.

If you want, you can check out the official documentation for more information.

Options

useFetch accepts a set of options as the last argument, which can be used to control the behavior of the composable.

Lazy

Data-fetching composables will automatically wait for the asynchronous function to resolve before navigating to a new page when using Vue's Suspense. Nuxt uses Vue’s <Suspense> component under the hood to prevent navigation before every async data is available to the view.

However, if you want to bypass this behavior during client-side navigation, you can use the lazy option:

Component.vue
1<script setup>
2const { pending, data: todos } = useFetch('/api/todos', {
3  lazy: true,
4})
5</script>
6
7<template>
8  <div v-if="pending">Loading ...</div>
9  <div v-else>
10    <div v-for="todo in todos">
11      {{ todo.name }}
12    </div>
13  </div>
14</template>

In such cases, you'll need to handle the loading state manually by using the pending value.

Alternatively, you have the option of using useLazyFetch which is a convenient method to achieve the same result:

const { pending, data: todos } = useLazyFetch('/api/todos')

Client-only

By default, data-fetching composables execute their asynchronous function in both client and server environments. To restrict the execution to the client side only, you can set the server option to false:

1const { pending, data: posts } = useFetch('/api/comments', {
2  lazy: true,
3  server: false,
4})

This can be particularly useful when combined with the lazy option for data that is not required during the initial rendering, such as non-SEO sensitive data.

Warning

If you have not fetched data on the server, for instance using server: false, the data will not be fetched until the hydration process is complete.

This implies that even if you await useFetch on the client side, the data variable will continue to be null within <script setup>.

Minimize payload size

The pick option helps you minimize the payload size stored in your HTML document by selecting the fields you want to be returned from the composables:

Component.vue
1<script setup>
2const { data: todos } = await useFetch('/api/todos', {
3  pick: ['id', 'name'],
4})
5</script>
6
7<template>
8  <div v-for="todo in todos">
9    <span>{{ todo.name }}</span>
10    <span>{{ todo.id }}</span>
11  </div>
12</template>

To gain more control or iterate over multiple objects, you can utilize the transform function to modify the query result:

1const { data: todos } = await useFetch('/api/todos', {
2  transform: (todos) => {
3    return todos.map((todo) => ({ name: todo.title, id: todo.description }))
4  },
5})

Refetching

To manually fetch or update data, you can employ the execute or refresh function offered by the composables:

Component.vue
1<script setup>
2const { data, error, execute, refresh } = await useFetch('/api/todos')
3</script>
4
5<template>
6  <div>
7    <p>{{ data }}</p>
8    <button @click="refresh">Refresh data</button>
9  </div>
10</template>

Both functions serve the same purpose, but execute is an alias for refresh and is more semantically suitable when immediate: false is used. When the immediate option is set to false (defaults to true), it will prevent the request from firing immediately.

Utilize the watch option to rerun your fetching function whenever other reactive values in your application undergo changes:

1const count = ref(1)
2
3const { data, error, refresh } = await useFetch('/api/todos', {
4  watch: [count],
5})

When to use refresh vs. a watch option?

Use refresh() when you are aware that the data on the server side has been modified, and you need to update the data on the client side accordingly.

When the user modifies parameters that need to be sent to the server, set those parameters as a watch source. For instance, if you want to filter API results using a search parameter, watch that parameter. This ensures that whenever users change their query, fresh and accurate data will be reloaded from the API.

Query Search Params

With the use of the query option, you can include search parameters in your query:

1const queryValue = ref('anyValue')
2
3const { data, pending, error, refresh } = await useFetch('/api/todos', {
4  query: { queryKey: queryValue, anotherQueryKey: 'anotherQueryValue' },
5})

This option is an extension of ofetch and leverages ufo to generate the URL. The objects provided are automatically converted to string format.

Interceptors

You can define async interceptors to hook into lifecycle events of the API call:

1const { data, pending, error, refresh } = await useFetch('/api/todo', {
2  onRequest({ request, options }) {},
3  onRequestError({ request, options, error }) {},
4  onResponse({ request, response, options }) {},
5  onResponseError({ request, response, options }) {},
6})

These options are provided by the built-in ofetch library.

useAsyncData

useFetch is designed to fetch data from a given URL, while useAsyncData allows for more intricate logic. Essentially, useFetch(url) is almost equivalent to useAsyncData(url, () => $fetch(url)), providing a more streamlined developer experience for the most common use case.

However, there are situations where employing the useFetch composable may not be suitable, such as when a CMS or a third-party service offers its own query layer. In such cases, you can utilize useAsyncData to encapsulate your calls and still enjoy the benefits provided by the composable:

const { data, error } = await useAsyncData('getTodos', () => fetchTodos())

In useAsyncData, the first argument serves as the unique key for caching the response obtained from the second argument, which is the querying function. However, if you prefer, you can omit this argument and directly pass the querying function itself. In such cases, the unique key will be automatically generated.

Info

Both useAsyncData and useFetch provide the same object type as their return value and accept a shared set of options as their last argument. These options allow you to customize the behavior of the composables, including features such as navigation blocking, caching, and execution control.

You can check out the official documentation for more information.

Conclusion

Let's summarize and explain when you should use which data fetching method:

  • The $fetch function is a suitable choice if you intend to initiate a network request based on user interaction. It is recommended to utilize $fetch when sending data to an event handler, performing client-side logic exclusively, or in conjunction with the useAsyncData composable.
  • The useFetch composable is the simplest approach to handle data fetching within a component's setup function.
  • If you require more precise control over the data fetching process, you can opt for useAsyncData in combination with $fetch.

If you liked this article, follow me on Twitter to get notified about new blog posts and more content from me.

Alternatively (or additionally), you can subscribe to my weekly Vue newsletter:

I will never share any of your personal data. You can unsubscribe at any time.

If you found this article helpful.You will love these ones as well.
Focus & Code Diff in Nuxt Content Code Blocks Image

Focus & Code Diff in Nuxt Content Code Blocks

Use Shiki to Style Code Blocks in HTML Emails Image

Use Shiki to Style Code Blocks in HTML Emails

How I Replaced Revue With a Custom-Built Newsletter Service Using Nuxt 3, Supabase, Serverless, and Amazon SES Image

How I Replaced Revue With a Custom-Built Newsletter Service Using Nuxt 3, Supabase, Serverless, and Amazon SES

Create a Table of Contents With Active States in Nuxt 3 Image

Create a Table of Contents With Active States in Nuxt 3