useFetch and useAsyncData

+15 Mana ✨

Nuxt provides composables for data fetching that work seamlessly with SSR.

useFetch

The primary way to fetch data in Nuxt:

vue
<script setup>
const { data, pending, error, refresh } = await useFetch('/api/users')
</script>

<template>
  <div v-if="pending">Loading...</div>
  <div v-else-if="error">Error: {{ error.message }}</div>
  <ul v-else>
    <li v-for="user in data" :key="user.id">{{ user.name }}</li>
  </ul>
</template>

useFetch Options

vue
<script setup>
const { data } = await useFetch('/api/users', {
  // HTTP method
  method: 'POST',
  
  // Request body
  body: { name: 'John' },
  
  // Query parameters
  query: { page: 1, limit: 10 },
  
  // Headers
  headers: { 'Authorization': 'Bearer token' },
  
  // Transform response
  transform: (response) => response.data,
  
  // Pick specific fields
  pick: ['id', 'name', 'email'],
  
  // Default value
  default: () => [],
  
  // Key for caching
  key: 'users-list',
  
  // Server only (no client fetch)
  server: true,
  
  // Lazy (don't block navigation)
  lazy: true,
  
  // Immediate (fetch on mount)
  immediate: true,
  
  // Watch sources to refetch
  watch: [page, filter]
})
</script>

useAsyncData

For custom async operations:

vue
<script setup>
const { data: user } = await useAsyncData('user', () => {
  return $fetch(`/api/users/${route.params.id}`)
})

// With options
const { data: posts } = await useAsyncData(
  'posts',
  () => fetchPosts(),
  {
    transform: (posts) => posts.filter(p => p.published),
    default: () => []
  }
)
</script>

Lazy Fetching

Don't block navigation while fetching:

vue
<script setup>
// useLazyFetch - doesn't block navigation
const { data, pending } = useLazyFetch('/api/posts')

// useLazyAsyncData
const { data: comments } = useLazyAsyncData('comments', fetchComments)
</script>

<template>
  <div v-if="pending">Loading posts...</div>
  <PostList v-else :posts="data" />
</template>

Refreshing Data

vue
<script setup>
const { data, refresh, pending } = await useFetch('/api/users')

// Manual refresh
async function handleRefresh() {
  await refresh()
}

// Clear cache and refetch
await refresh({ dedupe: true })
</script>

<template>
  <button @click="handleRefresh" :disabled="pending">
    {{ pending ? 'Refreshing...' : 'Refresh' }}
  </button>
</template>

Watching for Changes

vue
<script setup>
const page = ref(1)
const category = ref('all')

const { data: products } = await useFetch('/api/products', {
  query: { page, category },
  // Refetch when these change
  watch: [page, category]
})
</script>

<template>
  <select v-model="category">
    <option value="all">All</option>
    <option value="electronics">Electronics</option>
  </select>
  
  <button @click="page++">Next Page</button>
</template>

Error Handling

vue
<script setup>
const { data, error } = await useFetch('/api/users')

if (error.value) {
  // Handle error
  throw createError({
    statusCode: 404,
    message: 'Users not found'
  })
}
</script>

<template>
  <div v-if="error">
    <h2>Error</h2>
    <p>{{ error.message }}</p>
    <pre>{{ error.data }}</pre>
  </div>
</template>

$fetch for Non-Reactive Calls

vue
<script setup>
// For event handlers or non-reactive needs
async function createUser(userData: UserData) {
  const user = await $fetch('/api/users', {
    method: 'POST',
    body: userData
  })
  return user
}

async function deleteUser(id: number) {
  await $fetch(`/api/users/${id}`, {
    method: 'DELETE'
  })
}
</script>

Caching Behavior

vue
<script setup>
// Same key = shared data
const { data: user1 } = await useFetch('/api/user/1', { key: 'user-1' })
const { data: user2 } = await useFetch('/api/user/1', { key: 'user-1' })
// user1 and user2 share the same cached data

// Force fresh fetch
const { data: freshUser } = await useFetch('/api/user/1', {
  key: 'user-1-fresh',
  getCachedData: () => null  // Bypass cache
})
</script>
✓ Completed