Nuxt · · 5 min read

useFetch vs $fetch vs useAsyncData in Nuxt: when to use which

Nuxt has three ways to fetch data and picking the wrong one causes double requests and hydration bugs. Here is when to use useFetch, useAsyncData and $fetch.

By Mukesh Pensia, Vue.js & Nuxt developer

Nuxt gives you three tools for loading data, and they look interchangeable at first. They are not. Picking the wrong one is the most common cause of requests running twice and of hydration mismatches we see in Nuxt code reviews.

The short version

  • useFetch: loading data for a page or component from a URL. Your default.
  • useAsyncData: loading data with your own function, such as several calls, an SDK or a CMS client.
  • $fetch: anything triggered by the user, such as form submits, button clicks and mutations.

Why plain $fetch in setup is a problem

With server-side rendering, your component's setup runs twice: once on the server and again in the browser during hydration. Calling $fetch directly in setup means the request runs on both sides, and the browser can briefly render something different from the server HTML.

useFetch and useAsyncData fix this. The server fetches once, serialises the result into the page payload, and the browser reuses it without a second request.

useFetch: the default for page data

<script setup lang="ts">
const page = ref(1)

const { data: posts, status, error, refresh } = await useFetch('/api/posts', {
  query: { page },  // reactive, so it refetches when page changes
  transform: (posts) => posts.map((p) => ({ id: p.id, title: p.title })),
})
</script>

It is a wrapper around useAsyncData and $fetch that generates the cache key from the URL and options. Use transform to keep only the fields you need, because everything returned is shipped to the browser in the payload.

useAsyncData: when it isn't one URL

const { data } = await useAsyncData('dashboard', async () => {
  const [user, stats] = await Promise.all([
    $fetch('/api/me'),
    $fetch('/api/stats'),
  ])
  return { user, stats }
})

Give it an explicit, unique key. Two components using the same key share the same data, which is useful when you mean it and confusing when you don't.

$fetch: for user actions

async function subscribe() {
  await $fetch('/api/subscribe', {
    method: 'POST',
    body: { email: email.value },
  })
}

Composables like useFetch must run during setup, not inside event handlers. For anything that happens after the page has loaded, $fetch is the right tool.

Useful options

  • lazy: true (or useLazyFetch) doesn't block navigation. Show a loader while status === 'pending'.
  • server: false fetches only in the browser, for user-specific data that shouldn't be in the SSR HTML.
  • watch refetches when other reactive sources change.
  • refresh() refetches manually, for example after a mutation done with $fetch.

Rule of thumb: if the data is needed to render the page, use useFetch or useAsyncData. If a user action changes something, use $fetch.

Need a hand with your Nuxt app?

More notes