4 min read
On this page

useFetch & useAsyncData

Nuxt 3 provides two primary composables for fetching data: useFetch and useAsyncData. Both are SSR-aware, meaning they fetch data on the server during initial render, serialize the result into the HTML payload, and hydrate it on the client without a redundant second request. This is the foundation of Nuxt's data layer.

useFetch: The Primary Composable

useFetch is the most common way to fetch data in Nuxt. It wraps Nuxt's $fetch utility (which itself wraps ofetch) and handles caching, deduplication, SSR payload transfer, and reactive URL/parameter watching.

<script setup lang="ts">
const { data, status, error, refresh } = await useFetch('/api/products')
</script>

<template>
  <div>
    <p v-if="status === 'pending'">Loading products...</p>
    <p v-else-if="error">Failed to load: {{ error.message }}</p>
    <ul v-else>
      <li v-for="product in data" :key="product.id">
        {{ product.name }} - ${{ product.price }}
      </li>
    </ul>
  </div>
</template>

Return Values

useFetch returns a reactive object with these properties:

const {
  data,       // Ref<T | null> -- the response data
  status,     // Ref<'idle' | 'pending' | 'success' | 'error'>
  error,      // Ref<Error | null>
  refresh,    // () => Promise<void> -- re-executes the fetch
  execute,    // () => Promise<void> -- same as refresh
  clear,      // () => void -- clears data and resets state
} = await useFetch('/api/endpoint')

Typed Responses

Type the response to get full TypeScript support:

<script setup lang="ts">
interface Product {
  id: number
  name: string
  price: number
  category: string
}

const { data: products } = await useFetch<Product[]>('/api/products')
// products is Ref<Product[] | null>
</script>

Request Options

useFetch supports all $fetch options: method, headers, body, query parameters, and more:

<script setup lang="ts">
const category = ref('electronics')

const { data: products } = await useFetch('/api/products', {
  method: 'GET',
  query: { category, limit: 20 },
  headers: {
    'X-Custom-Header': 'value'
  }
})
</script>

When query contains reactive values (refs), useFetch automatically re-fetches when they change.

Transforming Responses

Use the transform option to reshape the API response before it reaches your component:

<script setup lang="ts">
const { data: productNames } = await useFetch('/api/products', {
  transform: (products) => products.map((p) => p.name)
})
// productNames is Ref<string[] | null>
</script>

The transform also reduces the payload size transferred from server to client, since only the transformed data is serialized.

useAsyncData: Lower-Level Control

useAsyncData is the lower-level composable that useFetch is built on. Use it when your data source is not a simple HTTP endpoint -- for example, when you need to combine multiple API calls, query a database directly, or run custom async logic.

<script setup lang="ts">
const { data: dashboard } = await useAsyncData('dashboard', async () => {
  const [stats, recentOrders, topProducts] = await Promise.all([
    $fetch('/api/stats'),
    $fetch('/api/orders', { query: { limit: 5 } }),
    $fetch('/api/products', { query: { sort: 'popular', limit: 5 } })
  ])

  return { stats, recentOrders, topProducts }
})
</script>

<template>
  <div>
    <StatsPanel :stats="dashboard?.stats" />
    <RecentOrders :orders="dashboard?.recentOrders" />
    <TopProducts :products="dashboard?.topProducts" />
  </div>
</template>

Key Differences

Feature useFetch useAsyncData
Data source HTTP endpoint via $fetch Any async function
Key Auto-generated from URL Must be provided manually
Watching Watches reactive query params Watches via explicit watch option
Use case Standard API calls Complex or multi-source fetching

useFetch('/api/products') is essentially shorthand for:

useAsyncData('products', () => $fetch('/api/products'))

Deduplication

Both composables deduplicate requests using a key. If two components on the same page call useFetch('/api/user'), the request fires only once:

<!-- components/UserAvatar.vue -->
<script setup lang="ts">
const { data: user } = await useFetch('/api/user')
</script>

<!-- components/UserGreeting.vue -->
<script setup lang="ts">
// Same URL = same key = no duplicate request
const { data: user } = await useFetch('/api/user')
</script>

For useAsyncData, provide the same string key to enable deduplication:

// Both resolve from the same cached data
const { data } = await useAsyncData('current-user', () => $fetch('/api/user'))

Lazy Fetching

By default, useFetch and useAsyncData block navigation until data is loaded. Lazy mode lets the page render immediately and loads data in the background:

<script setup lang="ts">
const { data: comments, status } = await useFetch('/api/comments', {
  lazy: true
})
</script>

<template>
  <div>
    <h2>Comments</h2>
    <p v-if="status === 'pending'">Loading comments...</p>
    <div v-else>
      <CommentCard v-for="comment in comments" :key="comment.id" :comment="comment" />
    </div>
  </div>
</template>

Nuxt also provides useLazyFetch and useLazyAsyncData as convenience wrappers:

// These are equivalent
const result1 = await useFetch('/api/data', { lazy: true })
const result2 = await useLazyFetch('/api/data')

Use lazy fetching for non-critical data (comments, recommendations, analytics) that should not delay page load.

Refresh & Execute

Both composables return refresh and execute functions that re-run the fetch:

<script setup lang="ts">
const { data: notifications, refresh } = await useFetch('/api/notifications')

// Manual refresh after a user action
async function markAllRead() {
  await $fetch('/api/notifications/mark-read', { method: 'POST' })
  await refresh()
}
</script>

<template>
  <div>
    <button @click="refresh">Refresh</button>
    <button @click="markAllRead">Mark All Read</button>
    <NotificationList :items="notifications" />
  </div>
</template>

Watching Reactive Sources

useFetch automatically re-fetches when reactive query parameters change. For useAsyncData, use the watch option:

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

const { data: results } = await useAsyncData(
  'search-results',
  () => $fetch('/api/search', { query: { q: search.value, page: page.value } }),
  { watch: [page, search] }
)
</script>

Server/Client Rendering & The Payload

The fundamental value of these composables is the SSR handoff. Here is what happens during a full page load:

1. Browser requests /products
2. Server runs useFetch('/api/products') -> fetches data
3. Server renders the HTML with the data already embedded
4. Server serializes the data into a <script> tag (the payload)
5. Browser receives the HTML (already showing products)
6. Client hydrates, reads the payload, skips the redundant fetch
7. Component is interactive with data already in place

On client-side navigation (via NuxtLink), the data is fetched from the client directly because the server is not involved in SPA transitions.

This pattern means:

  • The initial page load is fast and SEO-friendly because search engines see the rendered HTML.
  • No loading spinner flashes on the first visit.
  • Subsequent navigations still behave like a normal SPA.

Practical Example: Product Listing with Filters

<script setup lang="ts">
interface Product {
  id: number
  name: string
  price: number
  category: string
}

const selectedCategory = ref('all')
const sortBy = ref('name')

const { data: products, status } = await useFetch<Product[]>('/api/products', {
  query: {
    category: selectedCategory,
    sort: sortBy
  },
  transform: (raw) => {
    return raw.map((p) => ({
      ...p,
      priceFormatted: `$${p.price.toFixed(2)}`
    }))
  }
})
</script>

<template>
  <div>
    <div class="filters">
      <select v-model="selectedCategory">
        <option value="all">All Categories</option>
        <option value="electronics">Electronics</option>
        <option value="clothing">Clothing</option>
      </select>
      <select v-model="sortBy">
        <option value="name">Name</option>
        <option value="price">Price</option>
      </select>
    </div>

    <p v-if="status === 'pending'">Loading...</p>
    <ul v-else>
      <li v-for="product in products" :key="product.id">
        {{ product.name }} - {{ product.priceFormatted }}
      </li>
    </ul>
  </div>
</template>

Changing selectedCategory or sortBy triggers an automatic re-fetch because useFetch watches reactive query parameters.

Common Pitfalls

  • Calling useFetch outside <script setup>. These composables rely on the Nuxt component context. Calling them in a regular function or setTimeout throws errors. Always call them at the top level of <script setup> or inside another composable.
  • Forgetting that data can be null. Before the fetch resolves (or if it fails), data.value is null. Always handle the null case in templates and computed properties.
  • Not providing a key for useAsyncData. Without a unique key, deduplication and caching cannot work correctly. If two components use the same key with different fetch logic, one will overwrite the other.
  • Using $fetch directly in components. Calling $fetch without wrapping it in useAsyncData means no SSR payload transfer, no deduplication, and a redundant client-side request after hydration. Always use the composables for component data.
  • Blocking navigation with non-essential data. Large datasets or slow endpoints delay page transitions when fetched eagerly. Use lazy: true for data that is not critical for the initial render.

Key Takeaways

  • useFetch is the standard composable for HTTP data fetching. It wraps $fetch with SSR payload transfer, deduplication, and reactive watching.
  • useAsyncData is the lower-level alternative for custom async logic, multiple concurrent requests, or non-HTTP data sources.
  • Both composables fetch on the server, serialize the result into the page payload, and skip the redundant client-side request during hydration.
  • Lazy fetching (lazy: true or useLazyFetch) renders the page immediately and loads data in the background.
  • Reactive query parameters trigger automatic re-fetching. Use refresh() for manual re-fetching after user actions.
  • Always use these composables instead of raw $fetch in components to get the full benefit of Nuxt's SSR data handling.