Skip to content

useFetch

useFetch 是 Nuxt 中最常用的数据获取组合式函数,提供了 SSR 友好的数据获取方式。

为什么用 useFetch 而不是直接用 $fetch 或 axios?

问题$fetch / axiosuseFetch
SSR 时数据不会传给客户端❌ 客户端重复请求✅ 自动通过 Payload 传递
数据不是响应式的❌ 需要手动 ref 包裹✅ 返回 Ref,自动响应式
多组件重复请求同一数据❌ 各请求各的✅ 同 key 自动共享
加载状态管理❌ 手动管理 pending✅ 自动提供 status/pending
错误处理❌ 手动 try/catch✅ 自动提供 error ref

useFetch 做了什么?

  1. SSR 时:在服务端发起请求,获取数据用于渲染 HTML
  2. 将数据序列化到 Payload 中(嵌入 HTML)
  3. 客户端 Hydration 时:直接使用 Payload 数据,不再重复请求
  4. 后续客户端导航时:在客户端发起新请求

这就是"SSR 友好"的含义——同一个 useFetch 调用,在服务端和客户端都能正确工作。

基本用法

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

<template>
  <div v-if="status === 'pending'">加载中...</div>
  <div v-else-if="error">{{ error.message }}</div>
  <div v-else>
    <ul>
      <li v-for="user in data" :key="user.id">{{ user.name }}</li>
    </ul>
  </div>
</template>

await 在这里做了什么?

  • 在 SSR 时:等待数据返回后渲染 HTML(阻塞渲染,但确保页面有数据)
  • 在客户端首次加载时:从 Payload 读取数据(不阻塞)
  • 在客户端导航时:等待数据返回

status 的 4 个值

  • idle:还未开始(immediate: false 时)
  • pending:正在加载
  • success:加载成功
  • error:加载失败

返回值

属性类型说明使用场景
dataRef<T | undefined>响应数据模板中显示数据
statusRef<string>状态:idle/pending/success/error显示加载/错误状态
errorRef<Error | undefined>错误信息显示错误消息
pendingRef<boolean>是否正在加载等价于 status === 'pending'
refresh(opts?) => Promise<void>刷新数据用户手动刷新
execute(opts?) => Promise<void>refresh 的别名同上
clear() => void清除数据重置数据状态

data 是 Ref

模板中自动解包 在 <template> 中直接用 data,在 <script> 中要用 data.value

INFO

这是最常见的初学者错误

位置正确写法错误写法结果
<template>{{ data?.message }}{{ data.value?.message }}.value 多余但不会报错
<script>data.value.messagedata.message❌ 返回 undefined,因为 data 是 Ref 对象

原因:Vue 在模板中会自动解包 ref(datadata.value),但在 <script> 中不会。如果你在 <script> 中写 data.message,实际上访问的是 Ref 对象上的 message 属性(不存在),而不是响应式数据的 message 字段。

常用选项

method / query / body

ts
// GET 请求带查询参数
const { data } = await useFetch('/api/users', {
  query: { page: 1, size: 20 },
})

// POST 请求带请求体
const { data } = await useFetch('/api/users', {
  method: 'POST',
  body: { name: 'Alice', email: 'alice@example.com' },
})

query vs params

  • query → URL 查询参数,如 /api/users?page=1&size=20
  • body → 请求体,如 POST 的 JSON 数据
  • 没有 params 选项——路由参数直接写在 URL 中

headers

ts
const { data } = await useFetch('/api/me', {
  headers: {
    Authorization: `Bearer ${token.value}`,
  },
})

INFO

注意headers 中的值在 SSR 时也会被发送 如果 token 是用户特定的,确保 SSR 时也能正确获取

baseURL

ts
const { data } = await useFetch('/users', {
  baseURL: 'https://api.example.com',
})

推荐用 createUseFetch 代替手动设置 baseURL

这样所有请求都自动带上。

lazy

不阻塞导航,数据加载完后才显示:

ts
const { data } = await useFetch('/api/heavy-data', {
  lazy: true,
})

或使用 useLazyFetch

ts
const { data, pending } = useLazyFetch('/api/heavy-data')

lazy: true 的效果

页面立刻显示,data 初始为 undefined,需要用 pending 显示加载状态。适合非关键数据。

server

是否在服务端获取数据:

ts
// 仅在客户端获取(SSR 时不请求)
const { data } = await useFetch('/api/client-only', {
  server: false,
})

什么时候用 server: false

  • 不需要 SEO 的数据(用户个人信息、后台数据)
  • 依赖浏览器 API 的数据
  • 用户特定数据(SSR 时不知道是哪个用户)

INFO

server: falsedata 在 SSR 时为 undefined 需要处理这种情况或用 default 设置默认值

immediate

是否立即执行(默认 true):

ts
const { data, execute } = await useFetch('/api/search', {
  immediate: false,  // 不立即执行
})

// 手动触发
function search() {
  execute()
}

典型场景

搜索功能——页面加载时不搜索,用户输入关键词后点击"搜索"才执行。

watch

监听响应式源的变化,自动重新获取:

ts
const searchQuery = ref('')

const { data } = await useFetch('/api/search', {
  query: { q: searchQuery },
  watch: [searchQuery],  // searchQuery 变化时自动刷新
})

watch 的注意事项

  • 只有 watch 数组中的 ref 变化才会触发重新请求
  • query 中的 ref 不会自动触发——必须显式加入 watch
  • 可以传 false 禁用自动监听

常见模式

分页 + 搜索

ts
const page = ref(1)
const keyword = ref('')

const { data } = await useFetch('/api/users', {
query: { page, q: keyword },
watch: [page, keyword],  // 任一变化都刷新
})

transform

转换响应数据:

ts
const { data } = await useFetch('/api/users', {
  transform: (users) => {
    return users.map(u => ({ ...u, fullName: `${u.firstName} ${u.lastName}` }))
  },
})

transform vs 在模板中处理

  • transform:在数据传递到 Payload 前处理,减少 Payload 大小
  • 模板中处理:数据全量传递,在客户端处理

如果数据量很大

推荐用 transformpick 减少传递的数据 。

pick

只提取部分字段:

ts
const { data } = await useFetch('/api/user/1', {
  pick: ['name', 'email'],  // data 只包含 name 和 email
})

pick 的好处

API 返回了所有字段(包括密码哈希等),但客户端只需要 nameemailpick 确保只有这些字段被传递到 Payload 中,减少数据量和安全风险

default

设置默认值(数据加载前):

ts
const { data } = await useFetch('/api/users', {
  default: () => [],  // data 默认为空数组
})

为什么需要 default

data 初始值是 undefined,模板中 v-for="user in data" 会报错。设置 default: () => [] 后,data 初始为空数组,不会报错。

dedupe

避免重复请求:

ts
const { data } = await useFetch('/api/users', {
  dedupe: 'cancel',  // 取消之前的请求(默认)
  // dedupe: 'defer',  // 等待之前的请求完成,共享结果
})

cancel vs defer

  • cancel:新请求到来时,取消上一个未完成的请求。适合数据可能频繁变化的场景。
  • defer:如果已有相同 key 的请求在进行中,等待它完成并共享结果。适合数据不常变化的场景。

响应式 URL

使用计算属性或 ref 作为 URL,URL 变化时自动重新获取:

ts
const route = useRoute()
const id = computed(() => route.params.id as string)

const { data: post } = await useFetch(() => `/api/posts/${id.value}`)

为什么用函数形式?

直接写 /api/posts/${id.value} 时,URL 是静态字符串(只计算一次)。用函数形式 () => ... 时,URL 会在每次请求时重新计算,响应式变量的变化能被捕获。

类型化请求

ts
interface User {
  id: number
  name: string
}

// 自动类型推断(基于 API 路由的返回值)
const { data } = await useFetch('/api/users')

// 手动指定类型(API 路由推断不了时)
const { data } = await useFetch<User[]>('/api/users')

// 泛型 + transform
const { data } = await useFetch<User[], string[]>({
  url: '/api/users',
  transform: (users) => users.map(u => u.name),
})

拦截器

ts
const { data } = await useFetch('/api/login', {
  onRequest({ request, options }) {
    options.headers.set('Authorization', `Bearer ${token.value}`)
  },
  onRequestError({ request, options, error }) {
    console.error('请求错误', error)
  },
  onResponse({ request, response, options }) {
    console.log('响应状态', response.status)
  },
  onResponseError({ request, response, options }) {
    console.error('响应错误', response.status)
  },
})

拦截器 vs createUseFetch

拦截器适合单个请求的定制。如果你有多个请求需要相同的拦截逻辑(如添加鉴权头),推荐用 createUseFetch 创建自定义实例。

createUseFetch(v4.4+)

创建自定义的 useFetch 实例,预设默认选项:

ts
// app/composables/useApiFetch.ts
export const useApiFetch = createUseFetch((options) => {
  const config = useRuntimeConfig()
  return {
    ...options,
    baseURL: options.baseURL ?? config.public.apiBase,
    headers: {
      ...options.headers,
      Authorization: `Bearer ${useCookie('token').value}`,
    },
  }
})

使用方式和 useFetch 完全一样:

ts
const { data } = await useApiFetch('/users')

createUseFetch 解决了什么问题?

  • 每次请求都手动传 baseURLheaders?→ 创建自定义实例,预设这些选项
  • 所有请求都要鉴权头?→ 在自定义实例中统一添加
  • 401 自动跳转登录?→ 在自定义实例中统一处理

刷新数据

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

// 手动刷新
await refresh()

// 强制刷新(忽略缓存)
await refresh({ dedupe: false })

什么时候需要手动刷新?

  • 用户执行了操作后(如删除了一篇文章,需要刷新列表)
  • 轮询更新数据
  • 用户点击"刷新"按钮

常见问题

数据为 null?

如果 server: false,在 Hydration 完成前 datanull

ts
const { data } = await useFetch('/api/data', { server: false })
// 在服务端:data.value 为 undefined
// 在客户端 Hydration 后:data.value 才有值

TIP

解决方案:用 default: () => [] 设置默认值 或用 pending 显示加载状态

Hydration 不匹配?

避免在 useFetch 的 URL 中使用动态时间或随机值。

与 VueUse 的 useFetch 冲突?

确保不要 import { useFetch } from '@vueuse/core',Nuxt 的 useFetch 会自动导入。

知识脉络

text
视图与布局 → 你在这里:useFetch

              ├─→ 下一步:useAsyncData

              └─→ 相关:$fetch(非响应式请求)

基于 Nuxt 4 官方文档整理编写