在传统的 Vue 项目中,我们习惯用 Axios 封装数据请求,然后把返回的数据存进组件内的响应式变量。这种方式在简单场景下完全够用,但当应用变得复杂,几个棘手的问题会反复出现:
- 每个请求都要手动处理 loading、error 状态,导致模板里充斥着
v-if="loading"和v-if="error"的逻辑。 - 相同接口在不同组件中被多次请求,造成冗余的网络调用和数据不一致。
- 数据新鲜度难以管理:用户切到后台再回来时,页面上显示的还是几分钟前的旧数据。
- 乐观更新、分页加载、无限滚动这类交互需要大量样板代码,容易出错。
TanStack Query(在 Vue 生态中常被称为 Vue Query)正是为解决这些问题而生的服务端状态管理库。它将“数据如何获取、缓存、同步”这一层逻辑从组件中抽离出来,让你用声明式的方式管理服务端数据,彻底告别手动维护 loading/error/数据 三元组的日子。
核心概念:把服务端数据当作“缓存”来管理
TanStack Query 的核心思想是:服务端返回的数据本质上是一份“缓存”,它可能很快过时,也可能被多个组件共享。因此,它为你提供了开箱即用的缓存策略:
- 自动缓存:每个查询都会用唯一的
queryKey作为标识,相同 key 的请求会共享一份缓存数据,避免重复请求。 - 自动重新获取:当用户重新聚焦窗口、网络恢复、或者你手动使其“失效”时,Query 会自动重新请求数据,保证页面的新鲜度。
- 数据与状态一体化:每个查询都会向你提供
data、isLoading、isError、error等状态,无需手动维护这些标志位。
下面是一个使用 useQuery 获取用户列表的完整示例:
<template>
<div>
<div v-if="isLoading">加载中...</div>
<div v-else-if="isError">出错了:{{ error.message }}</div>
<ul v-else>
<li v-for="user in data" :key="user.id">
{{ user.name }}
</li>
</ul>
</div>
</template>
<script setup>
import { useQuery } from '@tanstack/vue-query'
import axios from 'axios'
const fetchUsers = async () => {
const { data } = await axios.get('/api/users')
return data
}
const { data, isLoading, isError, error } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
</script>
当你需要用不同参数查询列表时(比如分页、搜索),只需要让 queryKey 包含动态参数:
// 搜索关键词 searchTerm
const { data, isLoading } = useQuery({
queryKey: ['users', searchTerm],
queryFn: () => fetchUsers(searchTerm)
})
当 searchTerm 变化时,TanStack Query 会自动发起新的请求,并智能地缓存每个不同参数下的数据。
数据变更(Mutations):乐观更新与失效策略
除了查询数据,修改数据的请求(增删改)通常用 useMutation 处理。它提供 mutate 方法执行请求,更重要的是,你可以通过 onSuccess 回调来主动修改缓存,实现“乐观更新”或“失效后重新拉取”。
一个典型的场景:用户点赞一篇文章后,我们不希望用“等接口返回 → 再次请求列表”这种慢悠悠的体验,而是直接乐观更新本地缓存的点赞数:
import { useMutation, useQueryClient } from '@tanstack/vue-query'
const queryClient = useQueryClient()
const likeArticle = useMutation({
mutationFn: (articleId) => axios.post(`/api/articles/${articleId}/like`),
onMutate: async (articleId) => {
// 乐观更新:立即修改缓存中的点赞数
await queryClient.cancelQueries(['article', articleId])
const previous = queryClient.getQueryData(['article', articleId])
queryClient.setQueryData(['article', articleId], (old) => ({
...old,
likes: old.likes + 1
}))
return { previous }
},
onError: (err, articleId, context) => {
// 失败了就回滚
queryClient.setQueryData(['article', articleId], context.previous)
},
onSettled: () => {
// 最终与服务端保持同步
queryClient.invalidateQueries(['article'])
}
})
这种模式让界面响应极其灵敏,同时通过 invalidateQueries 确保了最终一致性。
高级功能:分页与无限滚动
TanStack Query 对常见的分页和无限滚动提供了内置支持,极大简化了代码逻辑。
分页(useQuery + 动态 pageNum):
将页码作为 queryKey 的一部分,同时在查询配置中设置 keepPreviousData,让切换页面时仍然显示上一页数据,避免布局抖动。
无限滚动(useInfiniteQuery):
你只需要告诉它如何获取“下一页”数据,以及如何从响应中提取游标/页码,useInfiniteQuery 就会自动帮你管理 data.pages 数组合并、加载更多状态、是否还有下一页等逻辑。
一个使用无限滚动加载商品列表的例子:
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery({
queryKey: ['products'],
queryFn: ({ pageParam = 1 }) => fetchProducts(pageParam),
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined
})
// 模板中监听滚动事件,触发 fetchNextPage()
用传统方式实现这些功能可能需要上百行状态管理代码,而 TanStack Query 让你专注于接口数据的形状。
与传统请求封装的对比
传统 Axios 封装通常会为你提供一个类似 useRequest 的自定义 hook,但它的核心仍然是“每个请求自己管理状态”。TanStack Query 的核心理念升级是将缓存、失效、去重、重试等策略变成框架级能力,而不是每个请求重写一遍。
| 维度 | 传统 Axios 封装 | TanStack Query |
|------|----------------|----------------|
| Loading/Error 状态 | 每次需手动维护 | 自动提供,与数据绑定 |
| 数据缓存与共享 | 需要自行实现全局 store | 基于 queryKey 自动缓存、去重 |
| 数据新鲜度 | 需要在生命周期钩子中手动刷新 | 支持窗口聚焦自动重取、staleTime 配置 |
| 乐观更新 | 手动编写数据回滚逻辑 | 提供 onMutate / invalidateQueries 内建支持 |
| 分页/无限滚动 | 繁琐的状态拼接 | useInfiniteQuery 一行搞定 |
| DevTools 支持 | 无 | 提供专用调试面板,可查看缓存状态 |
什么时候用 TanStack Query?
并不是所有数据请求都需要它。如果你的应用只是简单地从几个接口获取数据并展示,且几乎没有缓存共享和复杂同步需求,自己封装的 useRequest 完全够用。但当项目中满足以下任一条件时,强烈建议引入:
- 接口数量多,且在多个组件中复用 → 缓存共享利大于弊。
- 需要频繁的数据自动更新(如后台管理系统仪表盘) → staleTime + 自动重取让你不再手操心。
- 有复杂的乐观更新需求(如拖拽排序后本地立即修改顺序) → mutation 的乐观更新流程减少大量代码。
- 列表交互多(搜索、分页、无限滚动) → 直接用现有 hook,省出大量样板代码。
最后,一个真实的心得:TanStack Query 最让人舒服的不是它“功能多”,而是它让服务端数据的行为变得可预测。你知道缓存何时失效、用户操作后数据何时重新拉取,这种确定性在复杂项目中往往比任何花哨的特性都更重要。