数据获取

Nuxt 给了 4 个数据获取工具:useFetchuseAsyncDatauseLazyFetch$fetch。它们的共同目标是 —— 在服务端获取、客户端复用、按 key 去重 —— 但各自适用的场景不同。

useFetch —— 90% 的场景

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

<template>
  <p v-if="pending">加载中…</p>
  <p v-else-if="error">出错了</p>
  <ul v-else>
    <li v-for="p in data" :key="p.id">{{ p.title }}</li>
  </ul>
</template>

它自动做了:

  • SSR 阶段在服务端请求,客户端导航再走一次。
  • 按 URL + 选项缓存,多个组件可复用。
  • 合并并发请求。
  • 通过 Nitro 推导的 API 类型给响应加上类型。

常用选项:

await useFetch('/api/posts', {
  query: { page: 1 },
  headers: { 'x-role': 'admin' },
  transform: (posts) => posts.map(p => ({ ...p, shortTitle: p.title.slice(0, 32) })),
  pick: ['id', 'title'],
  watch: [search],           // 这些 ref 变化时重新请求
  default: () => [],         // 首次未返回前的默认值
})

useAsyncData —— 任意异步逻辑

不是简单 fetch(比如 URL 需要计算、要查询数据库、要合并多个来源),用 useAsyncData,并给一个稳定的 key

<script setup lang="ts">
const route = useRoute()
const { data: post } = await useAsyncData(
  () => `post-${route.params.id}`,
  () => $fetch(`/api/posts/${route.params.id}`),
  { watch: [() => route.params.id] }
)
</script>

其实 useFetch 就是 useAsyncData(key, () => $fetch(url, opts)) 的薄封装。

useLazyFetch —— 不阻塞导航

useFetch 默认 await,整个页面要等数据就位才渲染。次要内容可以用 useLazyFetch(或 useFetch(..., { lazy: true }))后台加载:

<script setup lang="ts">
const { data, pending } = useLazyFetch('/api/recommendations')
</script>

<template>
  <SkeletonList v-if="pending" />
  <RecommendationList v-else :items="data" />
</template>

$fetch —— 事件中的直连 HTTP

$fetch 是底层 HTTP 客户端(基于 ofetch),用来在事件处理器、提交动作、服务端路由里做命令式调用。不要拿来当页面主数据源 —— 它不参与 SSR payload。

<script setup lang="ts">
async function createPost () {
  return await $fetch('/api/posts', {
    method: 'POST',
    body: { title: '新文章' },
  })
}
</script>

重新拉取

useFetch / useAsyncData 都返回了 refresh()

const { data, refresh } = await useFetch('/api/posts')

async function addPost () {
  await $fetch('/api/posts', { method: 'POST', body: {/* … */} })
  await refresh()
}

其他组件里想刷新?用同一个 key:

await refreshNuxtData('posts')        // 按 key 刷新
await refreshNuxtData()               // 全部刷新

服务端路由:数据从哪里来

处理器放在 server/api/,文件名后缀映射 HTTP 方法:

// server/api/posts.get.ts
export default defineEventHandler(() => {
  return [{ id: 1, title: 'Hello' }]
})
// server/api/posts.post.ts
export default defineEventHandler(async (event) => {
  const body = await readBody<{ title: string }>(event)
  // 写库 …
  return { ok: true }
})

暴露为 /api/posts,页面里用 useFetch('/api/posts')$fetch('/api/posts', ...) 调用。

API 路由的类型

Nuxt 会自动根据 server/ 文件生成类型,useFetch$fetch 都能拿到响应类型:

const { data } = await useFetch('/api/posts') // data: Post[]

想导出响应类型复用:

// server/api/posts.get.ts
export type PostsResponse = Awaited<ReturnType<typeof handler>>
const handler = defineEventHandler(() => fetchPosts())
export default handler

选择原则

  • 页面级 SSR 数据?useFetch
  • 自定义异步逻辑 + 稳定缓存?useAsyncData
  • 次要的非阻塞数据?useLazyFetch / lazy: true
  • 用户触发的写操作?$fetch + refresh()
  • 多个组件要同一份数据? → 用相同的 key

照这个路子,页面就能 SSR 到位、预取顺滑、不做重复请求。

資料獲取

Nuxt 提供 4 種資料獲取工具:useFetchuseAsyncDatauseLazyFetch$fetch。共同目標為 —— 於伺服器獲取、客戶端重用、依 key 去重 —— 但各自適用情境不同。

useFetch —— 90% 情境

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

<template>
  <p v-if="pending">載入中…</p>
  <p v-else-if="error">發生錯誤</p>
  <ul v-else>
    <li v-for="p in data" :key="p.id">{{ p.title }}</li>
  </ul>
</template>

它自動處理:

  • SSR 階段於伺服器請求,客戶端導航再執行一次。
  • 以 URL + 選項快取,多個元件可重用。
  • 合併並發請求。
  • 透過 Nitro 推導的 API 型別賦予回應型別。

常用選項:

await useFetch('/api/posts', {
  query: { page: 1 },
  headers: { 'x-role': 'admin' },
  transform: (posts) => posts.map(p => ({ ...p, shortTitle: p.title.slice(0, 32) })),
  pick: ['id', 'title'],
  watch: [search],           // 這些 ref 變化時重新請求
  default: () => [],         // 首次回應前的預設值
})

useAsyncData —— 任意非同步邏輯

非單純 fetch(如 URL 需計算、要查資料庫、需合併多個來源)時使用 useAsyncData,並提供穩定的 key

<script setup lang="ts">
const route = useRoute()
const { data: post } = await useAsyncData(
  () => `post-${route.params.id}`,
  () => $fetch(`/api/posts/${route.params.id}`),
  { watch: [() => route.params.id] }
)
</script>

實際上 useFetch 就是 useAsyncData(key, () => $fetch(url, opts)) 的薄封裝。

useLazyFetch —— 不阻塞導航

useFetch 預設 await,整頁需等資料就緒才渲染。次要內容可用 useLazyFetch(或 useFetch(..., { lazy: true }))背景載入:

<script setup lang="ts">
const { data, pending } = useLazyFetch('/api/recommendations')
</script>

<template>
  <SkeletonList v-if="pending" />
  <RecommendationList v-else :items="data" />
</template>

$fetch —— 事件中的直連 HTTP

$fetch 是底層 HTTP 客戶端(基於 ofetch),用於事件處理器、提交動作、伺服器路由中的命令式呼叫。請勿當頁面主資料來源 —— 它不參與 SSR payload。

<script setup lang="ts">
async function createPost () {
  return await $fetch('/api/posts', {
    method: 'POST',
    body: { title: '新文章' },
  })
}
</script>

重新請求

useFetch / useAsyncData 皆回傳 refresh()

const { data, refresh } = await useFetch('/api/posts')

async function addPost () {
  await $fetch('/api/posts', { method: 'POST', body: {/* … */} })
  await refresh()
}

其他元件需刷新?共用相同 key:

await refreshNuxtData('posts')        // 依 key 刷新
await refreshNuxtData()               // 全部刷新

伺服器路由:資料從哪裡來

處理器置於 server/api/,檔名後綴對應 HTTP 方法:

// server/api/posts.get.ts
export default defineEventHandler(() => {
  return [{ id: 1, title: 'Hello' }]
})
// server/api/posts.post.ts
export default defineEventHandler(async (event) => {
  const body = await readBody<{ title: string }>(event)
  // 寫入資料庫 …
  return { ok: true }
})

暴露為 /api/posts,頁面內以 useFetch('/api/posts')$fetch('/api/posts', ...) 呼叫。

API 路由型別

Nuxt 會自動依 server/ 檔案產生型別,useFetch$fetch 皆可取得回應型別:

const { data } = await useFetch('/api/posts') // data: Post[]

欲匯出回應型別重用:

// server/api/posts.get.ts
export type PostsResponse = Awaited<ReturnType<typeof handler>>
const handler = defineEventHandler(() => fetchPosts())
export default handler

選擇原則

  • 頁面層級 SSR 資料?useFetch
  • 自訂非同步邏輯 + 穩定快取?useAsyncData
  • 次要非阻塞資料?useLazyFetch / lazy: true
  • 使用者觸發的寫入?$fetch + refresh()
  • 多個元件共享同一份資料? → 使用相同 key

如此一來,頁面可 SSR 正確、預取順暢、避免重複請求。

Data Fetching

Nuxt gives you four data-fetching primitives: useFetch, useAsyncData, useLazyFetch, and $fetch. They all share one goal — fetch on the server for SSR, hydrate on the client, and dedupe by key — but each solves a different case.

useFetch — the 90% case

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

<template>
  <p v-if="pending">Loading…</p>
  <p v-else-if="error">Something went wrong.</p>
  <ul v-else>
    <li v-for="p in data" :key="p.id">{{ p.title }}</li>
  </ul>
</template>

What it does for free:

  • Runs on the server during SSR and on the client during navigation.
  • Caches by URL + options (reused across components).
  • Deduplicates concurrent requests.
  • Types the response via Nitro's inferred API types.

Common options:

await useFetch('/api/posts', {
  query: { page: 1 },
  headers: { 'x-role': 'admin' },
  transform: (posts) => posts.map(p => ({ ...p, shortTitle: p.title.slice(0, 32) })),
  pick: ['id', 'title'],
  watch: [search],           // refetch when these refs change
  default: () => [],         // initial value before first resolution
})

useAsyncData — arbitrary async work

When you need more than a fetch (e.g. $fetch with computed URL, a DB query from a Nitro plugin, or composing several sources), reach for useAsyncData. Supply a stable cache key.

<script setup lang="ts">
const route = useRoute()
const { data: post } = await useAsyncData(
  () => `post-${route.params.id}`,
  () => $fetch(`/api/posts/${route.params.id}`),
  { watch: [() => route.params.id] }
)
</script>

useFetch is actually a thin wrapper around useAsyncData(key, () => $fetch(url, opts)).

useLazyFetch — don't block navigation

useFetch awaits by default — the page waits for data before rendering. For secondary sections that should load in the background, use useLazyFetch (or useFetch(..., { lazy: true })):

<script setup lang="ts">
const { data, pending } = useLazyFetch('/api/recommendations')
</script>

<template>
  <SkeletonList v-if="pending" />
  <RecommendationList v-else :items="data" />
</template>

$fetch — direct HTTP for events

$fetch is the underlying HTTP client (built on ofetch). Use it for imperative calls in event handlers, mutations, or server routes — not as your primary data source on a page (it won't participate in the SSR payload).

<script setup lang="ts">
async function createPost () {
  return await $fetch('/api/posts', {
    method: 'POST',
    body: { title: 'New post' },
  })
}
</script>

Refetching

useFetch / useAsyncData expose refresh():

const { data, refresh } = await useFetch('/api/posts')

async function addPost () {
  await $fetch('/api/posts', { method: 'POST', body: {/* … */} })
  await refresh()
}

Need to revalidate a useAsyncData call from elsewhere (e.g. after a mutation in another component)? Share the key:

await refreshNuxtData('posts')        // refresh by key
await refreshNuxtData()               // refresh everything

Server routes: where the data comes from

Place handlers in server/api/. Filenames map HTTP methods via a .<method>.ts suffix:

// server/api/posts.get.ts
export default defineEventHandler(() => {
  return [{ id: 1, title: 'Hello' }]
})
// server/api/posts.post.ts
export default defineEventHandler(async (event) => {
  const body = await readBody<{ title: string }>(event)
  // insert into DB…
  return { ok: true }
})

Exposed as /api/posts. Consumers use useFetch('/api/posts') or $fetch('/api/posts', ...).

Typing API routes

Nuxt auto-generates types from your server/ files, so both useFetch and $fetch know the response shape:

const { data } = await useFetch('/api/posts') // data: Post[]

To export the response type for reuse:

// server/api/posts.get.ts
export type PostsResponse = Awaited<ReturnType<typeof handler>>
const handler = defineEventHandler(() => fetchPosts())
export default handler

Rules of thumb

  • Page-level data for SSR?useFetch.
  • Custom async logic + stable cache?useAsyncData.
  • Non-blocking secondary data?useLazyFetch / lazy: true.
  • User-triggered mutation?$fetch + refresh().
  • Multiple components need the same data? → Same key in useAsyncData / useFetch.

Do this, and your pages are SSR-ready, prefetch-friendly, and never make the same request twice.