React: TanStack Query(React Query)
最后更新:2026-08-26
Tom 上节课用自定义 Hook + axios 封装了 HTTP 请求,但新问题又出现了:仪表盘和侧边栏都显示用户数量,两个组件各自发一次请求(浪费带宽和服务器资源);用户在 A 页面修改了用户名,切换到 B 页面时数据还是旧的;编辑表单提交后需要手动触发数据重取。他意识到:需要一个"服务端状态管理"方案,把 API 数据当作一种特殊的状态来管理——有缓存、有过期时间、能自动同步。
1. 你将学到
- useQuery 管理数据读取(获取、缓存、自动重取)
- useMutation 管理数据写入(增删改、失效查询)
- staleTime / cacheTime 控制缓存策略
- 乐观更新实现即时 UI 响应
- 服务端状态 vs 客户端状态的概念区分
2. 概念图解
flowchart TD
A[useQuery 调用] --> B{缓存命中?}
B -->|未命中| C[发起 API 请求]
B -->|命中但过期| C
B -->|命中且新鲜| D[直接返回缓存数据]
C --> E[缓存数据]
E --> F[渲染组件]
F --> G{staleTime 是否到期?}
G -->|未到期| H[数据标记为"新鲜"]
G -->|已到期| I[数据标记为"过期"]
I --> J{窗口切换回前台?}
J -->|是| C
I --> K{refetchInterval?}
K -->|是| C
I --> L{有新的 useMutation<br/>invalidate?}
L -->|是| C
style A fill:#e1f5fe,stroke:#0288d1
style E fill:#fff3e0,stroke:#f57c00
style H fill:#e8f5e9,stroke:#388e3c
style I fill:#ffcdd2,stroke:#d32f2f
TanStack Query 的核心机制:优先从缓存读取 → 标记新鲜/过期 → 过期时自动触发重新请求。
3. 一个真实场景
Tom 的仪表盘需要显示用户总数、订单总数、最近订单列表。这些数据来自三个不同的 API,且需要保持实时性——用户在其他页面修改了数据,回到仪表盘时应该看到最新的结果。此外,"添加商品"表单提交后,商品列表应该自动刷新,而不是手动刷新页面。
(1) 问题:手动管理缓存太难
不使用 TanStack Query 时,Tom 不得不自己处理:
// 手动管理缓存 — 每个组件都要写类似的逻辑
function Dashboard() {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/stats').then(res => res.json())
.then(d => { setData(d); setLoading(false) })
.catch(e => { setLoading(false) })
}, [])
// 问题 1:切到其他页面再回来 → 重新请求(浪费!)
// 问题 2:如果有另一个组件也需要同样数据 → 重复请求
// 问题 3:数据不会自动刷新,用户看到的可能是几分钟前的数据
}
TanStack Query 用一个 useQuery Hook 解决了上述所有问题。
| 特性 | 手动 fetch + useEffect | TanStack Query |
|---|---|---|
| 缓存管理 | 无缓存,切走即丢失 | 自动缓存,按 queryKey 管理 |
| 去重请求 | 多组件用同一数据 → 重复请求 | 自动去重,只请求一次 |
| 自动刷新 | 无 | 窗口聚焦/重连自动重取 |
| 后台更新 | 无 | staleTime 控制后台刷新 |
| 加载/错误状态 | 手动管理 loading/error | 自动提供 data/loading/error |
| 乐观更新 | 手动实现 | onMutate + onError 回滚 |
npm install @tanstack/react-query
(2) 初始化 Provider
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
// 创建 QueryClient(通常在 app 入口处)
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30 * 1000, // 默认 30 秒内数据视为"新鲜"
cacheTime: 5 * 60 * 1000, // 缓存保留 5 分钟
retry: 3, // 失败重试 3 次
refetchOnWindowFocus: true, // 窗口切换回前台时重取
}
}
})
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
)
}
4. useQuery:读取数据
▶ 示例 1:仪表盘数据获取
import { useQuery } from '@tanstack/react-query'
// 封装的 API 函数
async function fetchDashboardStats() {
const response = await fetch('/api/dashboard/stats')
if (!response.ok) throw new Error('获取统计数据失败')
return response.json()
}
function Dashboard() {
// useQuery 一行代码代替了 useState + useEffect + 手动缓存
const {
data: stats, // 响应数据
isLoading, // 首次加载中(无缓存时)
isFetching, // 是否正在请求(包括后台重取)
error, // 错误对象
refetch // 手动触发重取
} = useQuery({
queryKey: ['dashboardStats'], // 唯一标识,用于缓存匹配
queryFn: fetchDashboardStats, // 数据获取函数
staleTime: 30 * 1000, // 30 秒内不重新请求
retry: 3, // 失败重试 3 次
})
if (isLoading) return <DashboardSkeleton />
if (error) return <ErrorPanel message={error.message} onRetry={refetch} />
return (
<div className="dashboard">
<StatCard title="用户总数" value={stats.users} />
<StatCard title="订单总数" value={stats.orders} />
<StatCard title="总收入" value={`$${stats.revenue}`} />
</div>
)
}
// 侧边栏也显示用户数量 — 使用相同的 queryKey,不会重复请求!
function Sidebar() {
const { data: stats } = useQuery({
queryKey: ['dashboardStats'],
queryFn: fetchDashboardStats,
staleTime: 30 * 1000,
})
return (
<aside>
<p>在线用户:{stats?.onlineUsers ?? '...'}</p>
</aside>
)
}
关键机制:相同的 queryKey 共享同一份缓存。 Dashboard 和 Sidebar 使用相同的 ['dashboardStats'],TanStack Query 会自动去重——两个组件只会触发一次 API 请求,数据返回后同时更新两个组件。
▶ 示例 2:带参数的查询
function ProductDetail({ productId }) {
const { data, isLoading, error } = useQuery({
queryKey: ['product', productId], // queryKey 包含参数
queryFn: async () => {
const res = await fetch(`/api/products/${productId}`)
if (!res.ok) throw new Error('商品不存在')
return res.json()
},
enabled: !!productId, // productId 为空时不发请求
staleTime: 60 * 1000,
})
if (isLoading) return <p>加载商品详情...</p>
if (error) return <p>错误:{error.message}</p>
return (
<div>
<h2>{data.name}</h2>
<p className="price">${data.price}</p>
<p>{data.description}</p>
</div>
)
}
queryKey 中包含参数的意义: TanStack Query 使用 queryKey 做缓存的唯一标识。['product', 1] 和 ['product', 2] 是两条不同的缓存,互不干扰。当 productId 从 1 变为 2 时,优先从缓存读取 ['product', 2],如果有缓存且未过期就直接渲染,否则发请求。
▶ 示例:useQuery 返回的关键字段
| 字段 | 含义 | 使用场景 |
|---|---|---|
data |
最后一个成功的响应数据 | 渲染 UI |
isLoading |
首次加载且无缓存数据 | 显示首次加载骨架屏 |
isFetching |
任何正在进行的请求(包括后台重取) | 显示后台刷新指示器 |
error |
请求失败的错误对象 | 显示错误信息 |
refetch |
手动触发重新请求的函数 | "刷新"按钮 |
isStale |
数据是否已过期 | 条件性显示更新提示 |
5. useMutation:写入数据
读取用 useQuery,写入用 useMutation。这是 TanStack Query 的黄金法则。
▶ 示例 3:添加商品并刷新列表
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
// 商品列表查询
function useProducts() {
return useQuery({
queryKey: ['products'],
queryFn: async () => {
const res = await fetch('/api/products')
return res.json()
}
})
}
function ProductManager() {
const queryClient = useQueryClient()
// 添加商品的 mutation
const addProductMutation = useMutation({
mutationFn: async (newProduct) => {
const res = await fetch('/api/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newProduct)
})
if (!res.ok) throw new Error('添加失败')
return res.json()
},
// 成功后的操作:使商品列表缓存失效,触发重新请求
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] })
// 可选:同时显示成功提示
alert('商品添加成功!')
},
// 失败处理
onError: (error) => {
alert(`添加失败:${error.message}`)
}
})
// 数据
const { data: products, isLoading } = useProducts()
// 表单提交
function handleSubmit(event) {
event.preventDefault()
const formData = new FormData(event.target)
const newProduct = {
name: formData.get('name'),
price: Number(formData.get('price'))
}
addProductMutation.mutate(newProduct)
event.target.reset()
}
return (
<div>
<h2>商品管理</h2>
<form onSubmit={handleSubmit}>
<input name="name" placeholder="商品名称" required />
<input name="price" type="number" placeholder="价格" required />
<button type="submit" disabled={addProductMutation.isLoading}>
{addProductMutation.isLoading ? '提交中...' : '添加商品'}
</button>
</form>
{addProductMutation.isError && (
<p className="error">提交失败:{addProductMutation.error.message}</p>
)}
<hr />
<h3>商品列表</h3>
{isLoading && <p>加载中...</p>}
{products && (
<ul>
{products.map(p => (
<li key={p.id}>{p.name} — ${p.price}</li>
))}
</ul>
)}
</div>
)
}
核心流程: useMutation.mutate() 触发 POST 请求 → 服务器处理 → onSuccess 回调中执行 invalidateQueries → TanStack Query 自动重新请求 ['products'] 数据 → 列表 UI 自动更新。
为什么不是手动 setData? 调用 queryClient.setQueryData 可以手动更新缓存,但更好的做法是让 TanStack Query 通过 invalidateQueries 重新请求服务器数据——确保数据来源始终是服务器,避免前端缓存和服务端数据不一致。
6. 乐观更新
当网络延迟较高时,用户提交后需要等待服务器响应才能看到 UI 变化,体验不佳。乐观更新在请求发送前就立即更新 UI,如果请求失败再回滚到之前的状态。
▶ 示例 4:切换任务完成状态
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
// 获取待办列表
function useTodos() {
return useQuery({
queryKey: ['todos'],
queryFn: async () => {
const res = await fetch('/api/todos')
return res.json()
}
})
}
function TodoList() {
const queryClient = useQueryClient()
const { data: todos } = useTodos()
// 切换完成状态 — 使用乐观更新
const toggleMutation = useMutation({
// 实际的 API 请求
mutationFn: async ({ id, done }) => {
const res = await fetch(`/api/todos/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ done })
})
if (!res.ok) throw new Error('更新失败')
return res.json()
},
// ===== 乐观更新开始 =====
onMutate: async ({ id, done }) => {
// 1. 取消所有正在进行的 todos 查询(避免覆盖乐观更新)
await queryClient.cancelQueries({ queryKey: ['todos'] })
// 2. 保存当前缓存数据,用于失败时回滚
const previousTodos = queryClient.getQueryData(['todos'])
// 3. 立即更新缓存
queryClient.setQueryData(['todos'], (old) =>
old.map(todo =>
todo.id === id ? { ...todo, done } : todo
)
)
// 4. 返回旧数据用于回滚
return { previousTodos }
},
// ===== 乐观更新结束 =====
// 失败时回滚
onError: (err, variables, context) => {
if (context?.previousTodos) {
queryClient.setQueryData(['todos'], context.previousTodos)
}
},
// 无论成功还是失败,最终重新请求确保与服务端同步
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
}
})
return (
<ul>
{todos?.map(todo => (
<li key={todo.id} style={{ opacity: toggleMutation.isLoading ? 0.7 : 1 }}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleMutation.mutate({ id: todo.id, done: !todo.done })}
/>
<span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
{todo.title}
</span>
</li>
))}
</ul>
)
}
乐观更新三步曲:
onMutate:请求发送前立即更新缓存,让用户马上看到效果;保存旧数据以便回滚onError:请求失败时用保存的旧数据恢复缓存(回滚)onSettled:无论成功失败,最终从服务器重新请求数据,确保绝对一致性
7. 服务端状态 vs 客户端状态
理解 TanStack Query 的理念需要区分两种状态:
| 维度 | 服务端状态(Server State) | 客户端状态(Client State) |
|---|---|---|
| 来源 | 后端 API / 数据库 | 前端本地(用户操作) |
| 所有权 | 服务器拥有数据所有权 | 前端拥有所有权 |
| 持久性 | 存储在数据库 | 存储在内存或 localStorage |
| 同步需求 | 需要与服务器保持同步 | 不需要与服务器同步 |
| 更新方式 | 通过 API 写入 + 重新获取 | 直接 setState |
| 管理工具 | TanStack Query | Zustand / Redux Toolkit |
| 示例 | 用户列表、商品数据、订单信息 | 弹窗开关、表单输入值、主题色 |
核心原则:
- API 返回的数据(用户列表、商品信息、订单状态)→ 用 TanStack Query 管理
- 前端本地状态(弹窗开合、输入框值、主题设置)→ 用 Zustand / Context / useState 管理
▶ 示例:TanStack Query DevTools
TanStack Query 提供了专用的 DevTools 组件,可以在开发阶段查看所有查询的缓存状态、过期时间、最近更新时间。
npm install @tanstack/react-query-devtools
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
{/* 只在开发环境显示 DevTools */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
DevTools 的功能:
- 查看所有 queryKey 及其对应的缓存数据
- 查看每条缓存的 stale / active / inactive 状态
- 手动触发 refetch、invalidate、remove 操作
- 监控请求的网络耗时和响应大小
▶ 示例:QueryClient 的全局方法
除了 useQuery 和 useMutation,queryClient 对象还提供了多个全局方法,用于在组件外操作缓存:
import { queryClient } from './queryClient'
// 1. 使缓存失效(触发重新请求)
queryClient.invalidateQueries({ queryKey: ['products'] })
// 2. 清除所有缓存(用户登出时)
queryClient.clear()
// 3. 设置默认选项(全局修改 staleTime)
queryClient.setDefaultOptions({
queries: { staleTime: 60 * 1000 }
})
// 4. 获取缓存数据(不触发请求)
const cachedData = queryClient.getQueryData(['products'])
// 5. 预取数据(用户 hover 到链接时提前加载)
queryClient.prefetchQuery({
queryKey: ['product', '42'],
queryFn: () => fetchProduct('42')
})
// 用户点击链接后,数据已经在缓存中,瞬间渲染
// 6. 取消正在进行的查询
queryClient.cancelQueries({ queryKey: ['product', '42'] })
预取数据(prefetchQuery) 是提升用户体验的有效手段。当用户鼠标悬停在某个链接上时,提前发起请求,用户点击后数据已经到位,实现"瞬开"效果。
▶ 示例:何时需要自定义 QueryClient 配置
初始化时的 defaultOptions 定义了全局默认值,但每个 useQuery 调用可以单独覆盖:
// 全局默认:30 秒新鲜期
const queryClient = new QueryClient({
defaultOptions: {
queries: { staleTime: 30 * 1000, retry: 3 }
}
})
// 某个查询覆盖:数据几乎不变,新鲜期设为 10 分钟
function useProductCategories() {
return useQuery({
queryKey: ['categories'],
queryFn: fetchCategories,
staleTime: 10 * 60 * 1000, // 10 分钟内不重取
})
}
// 另一个查询覆盖:实时性要求高,禁用缓存
function useRealtimeNotifications() {
return useQuery({
queryKey: ['notifications'],
queryFn: fetchNotifications,
staleTime: 0, // 数据立刻过期
refetchInterval: 30 * 1000, // 每 30 秒自动轮询
})
}
配置原则: 全局配置设保守值(较短的 staleTime),每个查询根据数据特性单独覆盖。高频变动的数据(通知、实时统计)设短的 staleTime 甚至开启轮询;低频变动的数据(分类列表、配置信息)设长的 staleTime 减少请求。
❓ 常见问题
invalidateQueries——使缓存失效,让 TanStack Query 重新请求服务器数据,保证数据一致性。setQueryData 适合极少变更且已知返回格式的场景(如客户端唯一 ID 生成)。如果需要立即显示用户操作的结果(不等待服务器响应),用乐观更新(onMutate + 回滚)代替 setQueryData。📖 小节
- useQuery 管理数据读取:自动缓存、去重请求、后台重取、失败重试
- useMutation 管理数据写入:配合 invalidateQueries 自动触发数据刷新
- staleTime 控制数据新鲜度,cacheTime 控制缓存保留时间
- 乐观更新通过 onMutate 先更新 UI、onError 回滚、onSettled 最终同步,提升用户体验
- 服务端状态(API 数据)用 TanStack Query,客户端状态(UI 状态)用 Zustand/Context
📝 作业
- 用 useQuery 创建一个用户列表组件:使用
https://jsonplaceholder.typicode.com/users作为 API,实现缓存共享(两个组件使用相同 queryKey,验证只发一次请求),并添加手动刷新按钮。 - 用 useMutation 实现"添加商品"功能:表单提交后触发 POST 请求,成功后自动刷新商品列表,失败时显示错误提示。
- 实现一个"切换待办事项完成状态"的乐观更新:点击 checkbox 立即切换状态,如果 API 请求失败则回滚。写完后测试断开网络点击 checkbox,观察回滚效果。