2026 年,npm 周下载量突破 1200 万的 TanStack Query(前身 React Query)已经成为 React、Vue、Solid、Svelte 生态中数据获取的事实标准。它不是又一个 Axios 封装,而是一套完整的异步状态管理系统——自动缓存、后台刷新、乐观更新、离线支持、分页预取,这些过去需要数百行手写代码才能实现的功能,现在只需一个 useQuery Hook 就能搞定。如果你还在用 useEffect + useState + fetch 的老三样管理服务端数据,这篇文章将彻底改变你的前端数据层架构。
💡 提示:TanStack Query 管理的是服务端状态(Server State)——来自 API 的、可能过期的、需要同步的异步数据。它和 Zustand、Jotai 等客户端状态管理库不是竞争关系,而是互补关系。
🔧 一、核心机制:为什么 useQuery 不只是 fetch 的封装
1.1 传统方案的三大痛点
在没有 TanStack Query 的年代,一个简单的数据获取组件通常长这样:
// ❌ 传统方案:useEffect + fetch 的典型写法
import { useState, useEffect } from 'react'
function UserProfile({ userId }) {
const [data, setData] = useState(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let cancelled = false
setIsLoading(true)
fetch(`/api/users/${userId}`)
.then(res => {
if (!res.ok) throw new Error('请求失败')
return res.json()
})
.then(data => {
if (!cancelled) {
setData(data)
setIsLoading(false)
}
})
.catch(err => {
if (!cancelled) {
setError(err)
setIsLoading(false)
}
})
return () => { cancelled = true }
}, [userId])
if (isLoading) return <div>加载中...</div>
if (error) return <div>出错了:{error.message}</div>
return <div>{data.name}</div>
}
这段代码有三个致命问题:
- ❌ 没有缓存:同一个用户数据,切换页面再回来要重新请求
- ❌ 没有后台刷新:数据不会自动更新,用户看到的永远是旧数据
- ❌ 竞态条件:快速切换 userId 时,后发的请求可能先返回导致数据错乱
1.2 TanStack Query 的解决方案
// ✅ TanStack Query:同样的功能,3 行搞定
import { useQuery } from '@tanstack/react-query'
function UserProfile({ userId }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
})
if (isLoading) return <div>加载中...</div>
if (error) return <div>出错了:{error.message}</div>
return <div>{data.name}</div>
}
看起来只是少了几行代码?实际上背后发生了质变:
| 特性 | useEffect + fetch | TanStack Query |
|---|---|---|
| 自动缓存 | ❌ 无 | ✅ 按 queryKey 缓存 |
| 后台刷新 | ❌ 无 | ✅ stale-while-revalidate |
| 竞态处理 | ⚠️ 手动 cancelled | ✅ 自动处理 |
| 窗口聚焦刷新 | ❌ 无 | ✅ 默认开启 |
| 重试机制 | ⚠️ 手动实现 | ✅ 默认重试 3 次 |
| 分页预取 | ❌ 无 | ✅ 自动预取下一页 |
| 离线支持 | ❌ 无 | ✅ 内置持久化 |
| 代码量(中型项目) | ~500 行 | ~50 行 |
📌 **记住:**TanStack Query 的
queryKey不只是标识符——它是缓存系统的核心。['user', userId]意味着每个 userId 都有独立的缓存条目,切换 userId 时自动使用对应缓存,同时在后台检查数据是否过期。
1.3 Stale-While-Revalidate 策略解析
TanStack Query 的缓存策略来自 HTTP 的 stale-while-revalidate 模型:
用户请求数据 → 有缓存且未过期 → 直接返回缓存(无网络请求)
用户请求数据 → 有缓存但已过期 → 先返回缓存,后台静默刷新
用户请求数据 → 无缓存 → loading 状态,发起网络请求
这个策略的核心思想是:用户永远不需要等待网络请求完成才能看到内容。对于内容型应用,这意味着页面切换瞬间完成,同时数据保持最新。
// TypeScript 完整配置示例
import { useQuery } from '@tanstack/react-query'
interface User {
id: number
name: string
email: string
avatar: string
}
function useUser(userId: number) {
return useQuery<User>({
queryKey: ['user', userId],
queryFn: async () => {
const res = await fetch(`/api/users/${userId}`)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
},
staleTime: 5 * 60 * 1000, // 5 分钟内认为数据新鲜,不触发后台刷新
gcTime: 30 * 60 * 1000, // 缓存保留 30 分钟(旧称 cacheTime)
retry: 2, // 失败重试 2 次
refetchOnWindowFocus: true, // 窗口聚焦时刷新
})
}
⚠️ 警告:
staleTime和gcTime是两个完全不同的概念。staleTime控制"数据何时过期"(默认 0,即立即过期),gcTime控制"未使用的缓存何时被垃圾回收"(默认 5 分钟)。很多开发者把这两个搞混,导致缓存行为不符合预期。
🚀 二、进阶模式:Mutation、乐观更新与无限滚动
2.1 useMutation:写操作的标准范式
useQuery 负责读,useMutation 负责写。但 Mutation 的核心价值不在"发送请求",而在请求成功后自动更新缓存。
// ✅ useMutation 标准用法:创建用户后自动刷新列表
import { useMutation, useQueryClient } from '@tanstack/react-query'
function useCreateUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (newUser: Omit<User, 'id'>) => {
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newUser),
})
if (!res.ok) throw new Error('创建失败')
return res.json()
},
// ✅ 成功后自动刷新用户列表缓存
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
},
})
}
// 使用方式
function CreateUserForm() {
const createUser = useCreateUser()
const handleSubmit = (formData: FormData) => {
createUser.mutate(
{ name: formData.get('name') as string, email: formData.get('email') as string },
{
onSuccess: () => {
// 表单重置、提示成功等
},
onError: (error) => {
// 错误处理
},
}
)
}
return (
<form onSubmit={handleSubmit}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit" disabled={createUser.isPending}>
{createUser.isPending ? '创建中...' : '创建用户'}
</button>
</form>
)
}
2.2 乐观更新:让 UI 快过网络
乐观更新(Optimistic Update)是用户体验的杀手级特性——在服务器响应之前就更新 UI,如果请求失败则回滚。点赞、收藏、拖拽排序等场景必备。
// ✅ 乐观更新实战:点赞功能
import { useMutation, useQueryClient } from '@tanstack/react-query'
function useLikePost(postId: number) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async () => {
const res = await fetch(`/api/posts/${postId}/like`, { method: 'POST' })
if (!res.ok) throw new Error('点赞失败')
return res.json()
},
// 🚀 在 mutation 执行前,立即更新缓存(乐观更新)
onMutate: async () => {
// 1. 取消正在进行的查询,防止覆盖我们的乐观更新
await queryClient.cancelQueries({ queryKey: ['post', postId] })
// 2. 保存当前值,用于失败时回滚
const previousPost = queryClient.getQueryData(['post', postId])
// 3. 乐观地更新缓存
queryClient.setQueryData(['post', postId], (old: any) => ({
...old,
likes: old.likes + 1,
isLiked: true,
}))
return { previousPost }
},
// ❌ 失败时回滚到之前的值
onError: (_err, _variables, context) => {
if (context?.previousPost) {
queryClient.setQueryData(['post', postId], context.previousPost)
}
},
// ✅ 无论成功或失败,最终都重新获取最新数据
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['post', postId] })
},
})
}
⚠️ **警告:**乐观更新的
onMutate中必须调用cancelQueries,否则正在进行的后台刷新可能覆盖你的乐观更新,导致 UI 闪烁。这是最常见的乐观更新 Bug。
2.3 useInfiniteQuery:无限滚动的优雅实现
无限滚动(Infinite Scroll)是现代应用的标配,但手动实现分页状态管理极其繁琐。useInfiniteQuery 把这一切封装好了:
// ✅ 无限滚动:自动管理分页、预取下一页
import { useInfiniteQuery } from '@tanstack/react-query'
interface Post {
id: number
title: string
content: string
}
interface PostPage {
data: Post[]
nextCursor: string | null // 游标分页
}
function useInfinitePosts() {
return useInfiniteQuery({
queryKey: ['posts'],
queryFn: async ({ pageParam }) => {
const res = await fetch(`/api/posts?cursor=${pageParam}&limit=20`)
if (!res.ok) throw new Error('加载失败')
return res.json() as Promise<PostPage>
},
initialPageParam: '',
getNextPageParam: (lastPage) => lastPage.nextCursor, // 返回 null 时停止预取
})
}
// 组件中使用
function PostFeed() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfinitePosts()
return (
<div>
{data?.pages.flatMap(page => page.data).map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? '加载中...' : hasNextPage ? '加载更多' : '没有更多了'}
</button>
</div>
)
}
💡 **提示:**TanStack Query 会自动预取下一页数据——当用户看到"加载更多"按钮时,下一页数据可能已经在后台请求完成了。这个行为可以通过
prefetchInfiniteQuery手动控制。
💡 三、实战架构:与框架集成的最佳实践
3.1 查询客户端配置:全局统一管理
// ✅ 推荐的项目结构
// lib/query-client.ts
import { QueryClient } from '@tanstack/react-query'
export function createQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 全局默认 1 分钟过期
gcTime: 5 * 60 * 1000, // 缓存保留 5 分钟
retry: 1, // 默认重试 1 次
refetchOnWindowFocus: false, // 生产环境建议关闭,避免不必要的请求
},
mutations: {
retry: 0, // 写操作不重试,避免重复提交
},
},
})
}
3.2 与 Axios 搭配:统一错误处理
// ✅ 推荐:用 Axios 作为 queryFn 底层
import axios from 'axios'
import { useQuery } from '@tanstack/react-query'
const api = axios.create({ baseURL: '/api' })
// 全局拦截器处理 401、网络错误等
api.interceptors.response.use(
(res) => res,
(err) => {
if (err.response?.status === 401) {
// 跳转登录页
window.location.href = '/login'
}
return Promise.reject(err)
}
)
// queryFn 直接用 axios
function useUser(userId: number) {
return useQuery({
queryKey: ['user', userId],
queryFn: async ({ signal }) => {
const { data } = await api.get(`/users/${userId}`, { signal })
return data
},
})
}
📌 **记住:**一定要在
queryFn中解构出signal并传给 axios/fetch,这样 TanStack Query 才能在组件卸载时自动取消请求,避免内存泄漏和状态更新错误。
3.3 开发者工具:调试利器
// ✅ 安装 React Query Devtools(仅开发环境)
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
</QueryClientProvider>
)
}
Devtools 可以实时查看:每个查询的缓存状态(fresh / stale / fetching)、缓存数据内容、请求时间线、活跃观察者数量。在排查"数据不更新"或"请求重复发送"问题时,Devtools 比 console.log 高效 10 倍。
📊 四、TanStack Query vs SWR vs RTK Query 选型对比
| 维度 | TanStack Query | SWR | RTK Query |
|---|---|---|---|
| 框架支持 | React/Vue/Solid/Svelte/Angular | React/Vue/Svelte | React/Angular |
| 缓存策略 | stale-while-revalidate | stale-while-revalidate | 基于标签失效 |
| 乐观更新 | ✅ 原生支持 | ⚠️ 需手动实现 | ✅ 原生支持 |
| 无限滚动 | ✅ useInfiniteQuery | ⚠️ 社区方案 | ❌ 不支持 |
| 离线持久化 | ✅ 插件支持 | ❌ 不支持 | ❌ 不支持 |
| DevTools | ✅ 功能丰富 | ✅ 基础 | ✅ RTK DevTools |
| 包大小 | ~13KB (gzip) | ~4KB (gzip) | ~15KB (gzip) |
| 学习曲线 | 中等 | 低 | 高(需 Redux) |
| TypeScript | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 社区生态 | 最活跃 | 活跃 | Redux 生态 |
| GitHub Stars | 43K+ | 30K+ | Redux Toolkit 统一 |
⚡ **关键结论:**如果你的项目不限制技术栈,TanStack Query 是 2026 年的默认选择。SWR 适合小项目或只需要简单数据获取的场景,RTK Query 适合已经在用 Redux 的项目。
⚠️ 五、常见陷阱与避坑指南
5.1 queryKey 设计错误
// ❌ 错误:queryKey 包含对象引用(每次渲染都不同,导致无限请求)
useQuery({ queryKey: ['user', { id: userId }], queryFn: ... })
// ✅ 正确:queryKey 使用原始值
useQuery({ queryKey: ['user', userId], queryFn: ... })
// ✅ 复杂场景:使用数组展平
useQuery({ queryKey: ['posts', { page, limit, sort }], queryFn: ... })
5.2 忘记处理 loading 和 error 状态
// ❌ 错误:只处理了 data,没有处理 loading 和 error
const { data } = useQuery(...)
return <div>{data.name}</div> // data 可能是 undefined!
// ✅ 正确:使用 status 判断
const { data, status } = useQuery(...)
if (status === 'pending') return <Skeleton />
if (status === 'error') return <ErrorMessage />
return <div>{data.name}</div>
5.3 Mutation 后忘记失效缓存
// ❌ 错误:Mutation 成功后没有刷新相关查询
useMutation({ mutationFn: deleteUser })
// ✅ 正确:失效相关缓存
useMutation({
mutationFn: deleteUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
// 或者精确失效
queryClient.invalidateQueries({ queryKey: ['users', 'list'] })
},
})
5.4 组件内创建 QueryClient
// ❌ 错误:每次渲染都创建新的 QueryClient
function App() {
const queryClient = new QueryClient() // 每次渲染都重建缓存!
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}
// ✅ 正确:使用 useMemo 或在组件外创建
const queryClient = new QueryClient()
function App() {
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}
🎯 六、性能优化与最佳实践
**查询依赖(Dependent Queries):**当一个查询的参数依赖另一个查询的结果时,使用 enabled 选项:
// ✅ 先获取用户,再获取该用户的仓库
function useUserRepos(userId: number) {
const user = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
})
const repos = useQuery({
queryKey: ['repos', userId],
queryFn: () => fetchRepos(userId),
enabled: !!user.data, // user 数据加载完成后才触发
})
return { user, repos }
}
**预取(Prefetching):**在用户即将访问某个页面前,提前加载数据:
// ✅ 鼠标悬停时预取详情页数据
function PostLink({ postId }: { postId: number }) {
const queryClient = useQueryClient()
const handleMouseEnter = () => {
queryClient.prefetchQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
staleTime: 60 * 1000, // 1 分钟内不重复预取
})
}
return <Link to={`/posts/${postId}`} onMouseEnter={handleMouseEnter}>查看</Link>
}
**全局错误处理:**设置全局的错误回调,统一处理 401、网络断开等场景:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
throwOnError: (error) => {
// 对于严重错误(如 401),抛出让 Error Boundary 捕获
if (error instanceof ApiError && error.status === 401) {
return true
}
return false
},
},
},
})
✅ 总结
TanStack Query 不是一个"可选的第三方库"——在 2026 年的前端开发中,它和 TypeScript 一样,已经成为项目基础设施的一部分。它的核心价值在于:把服务端状态管理从"手动同步"变成"声明式描述"。你只需要告诉 TanStack Query"我要什么数据",它会自动处理"什么时候获取、什么时候刷新、什么时候缓存、什么时候失效"。
起步建议:
- ✅ 新项目直接用 TanStack Query v5(当前最新稳定版)
- ✅ 从
useQuery开始,逐步引入useMutation和乐观更新 - ✅ 安装 DevTools,养成用 DevTools 调试缓存的习惯
- ❌ 不要和 SWR 混用——选一个,坚持用
- ❌ 不要在 queryFn 中做业务逻辑——它只负责获取数据
⚡ **关键结论:**如果你今天只能学一个新的前端库,选 TanStack Query。它解决的不是"怎么发请求"的问题,而是"怎么让服务端数据和 UI 保持同步"的问题——这是每个前端应用都必须面对的核心挑战。