TanStack Query
# 基础使用
# 配置
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 数据 60 秒内视为新鲜
gcTime: 5 * 60 * 1000, // 未使用的缓存 5 分钟后回收
retry: 3, // 失败重试 3 次
refetchOnWindowFocus: true, // 窗口聚焦时重新获取
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<MainApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 基础前置概念
useQuery:普通单次查询(详情、单页列表)
useInfiniteQuery:无限分页滚动查询
useMutation:新增、编辑、删除、表单提交
# useQuery
数据请求
基础语法
const { data, // 请求成功返回的数据 isLoading, // 初次加载 loading isFetching, // 任意请求(初次/后台刷新) isError, // 请求失败 error, // 错误对象 refetch, // 手动重新请求 status, // 'loading' | 'success' | 'error' } = useQuery({ queryKey: ['user', id], // 唯一缓存标识,参数变化自动重请求 queryFn: () => getUserApi(id), // 请求函数,必须返回 Promise // 会继承你全局配置 staleTime/gcTime/retry staleTime: 60 * 1000, });1
2
3
4
5
6
7
8
9
10
11
12
13
14关键参数
queryKey queryFn1
2最简示例
import { useQuery } from '@tanstack/react-query'; const getUserInfo = async (userId) => { const res = await fetch(`/api/user/${userId}`); return res.json(); }; function UserInfo({ userId }) { const { data, isLoading, isError } = useQuery({ queryKey: ['user', userId], queryFn: () => getUserInfo(userId), }); if (isLoading) return <div>加载用户信息...</div>; if (isError) return <div>加载失败</div>; return <div>用户名:{data.name}</div>; }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18用手动操作
// 强制刷新接口,忽略缓存 refetch() // 清除缓存,下次渲染重新请求 queryClient.invalidateQueries(['user', userId])1
2
3
4
# useInfiniteQuery
分页增强版
基础语法
const { data, // { pages: 每页数据[], pageParams: 分页参数[] } fetchNextPage, // 加载下一页 hasNextPage, // 是否存在下一页 isFetchingNextPage, // 加载下一页loading isLoading, // 第一页初次加载 } = useInfiniteQuery({ queryKey: ['article', type], // pageParam 分页标识,默认初始值由 getNextPageParam 控制 queryFn: ({ pageParam = 1 }) => getListApi(pageParam), // 核心配置:提取下一页参数,返回 null/undefined 代表无更多 getNextPageParam: (lastPage) => lastPage.nextPage, });1
2
3
4
5
6
7
8
9
10
11
12
13
# useMutation
数据修改
基础语法
const { mutate, // 触发提交的函数(同步,不阻塞渲染) mutateAsync, // 返回Promise,支持await isPending, // 请求进行中loading isSuccess, // 请求成功 isError, // 请求失败 error, } = useMutation({ mutationFn: (params) => submitApi(params), // 提交接口 // 生命周期回调 onSuccess: (res, variables) => { // 请求成功后刷新列表缓存,更新页面数据 queryClient.invalidateQueries(['article']); }, onError: (err) => { console.error('提交失败', err); }, });1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18参数说明
// mutationFn 接收调用 mutate(参数) 传入的变量,执行提交请求 // mutate 回调形式 mutate(formData, { onSuccess: () => alert('提交成功') }) // mutateAsync 支持 async await const handleSubmit = async () => { await mutateAsync(formData); router.push('/list'); }1
2
3
4
5
6
7
8
9
10
11
12
13完整示例
import { useMutation, useQueryClient } from '@tanstack/react-query'; const addArticleApi = async (data) => { const res = await fetch('/api/article', { method: 'POST', body: JSON.stringify(data) }); return res.json(); }; function AddArticle() { const queryClient = useQueryClient(); const { mutate, isPending } = useMutation({ mutationFn: addArticleApi, onSuccess: () => { // 失效列表缓存,页面自动刷新最新数据 queryClient.invalidateQueries(['article']); }, }); const submit = () => { mutate({ title: '新文章', content: '内容' }); }; return <button onClick={submit} disabled={isPending}>{isPending ? '提交中' : '新增'}</button>; }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26乐观更新
提交前直接修改缓存,无需等待接口返回,提升体验,失败自动回滚
// 纯乐观更新,不重新请求 useMutation({ mutationFn: addArticleApi, onMutate: async (newItem) => { await queryClient.cancelQueries(['article']); // 保存旧数据用于回滚 const oldData = queryClient.getQueryData(['article']); // 手动修改缓存,提前展示新增数据 queryClient.setQueryData(['article'], (old) => ({ ...old, pages: [{ list: [newItem, ...old.pages[0].list] }, ...old.pages.slice(1)] })); return { oldData }; }, // 请求失败,恢复原始缓存 onError: (err, vars, context) => { queryClient.setQueryData(['article'], context.oldData); }, }); // 乐观更新 + 成功后刷新真实数据 useMutation({ mutationFn: addArticleApi, onMutate: async (newItem) => { await queryClient.cancelQueries(['article']); const oldData = queryClient.getQueryData(['article']); queryClient.setQueryData(['article'], (old) => ({ ...old, pages: [{ list: [newItem, ...old.pages[0].list] }, ...old.pages.slice(1)] })); return { oldData }; }, onError: (err, vars, context) => { queryClient.setQueryData(['article'], context.oldData); }, // 请求完成(无论成功失败),重新拉取真实列表覆盖本地临时数据 onSettled: () => { queryClient.invalidateQueries(['article']); } });1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40注意事项
Mutation 成功后必须
invalidateQueries刷新对应 Query,保证页面数据同步;
# 常用操作
queryClient是全局缓存控制器,用来手动操控缓存、触发请求,分为四大类:失效刷新、直接修改缓存、预加载、其他辅助
# invalidateQueries
标记缓存过期,自动重发请求
// 清空全部
queryClient.invalidateQueries();
// 前缀匹配失效
// 匹配所有以 todos 开头的缓存:['todos']、['todos', 1]、['todos', page] 都会失效。
queryClient.invalidateQueries({ queryKey: ['todos'] });
// exact: true 精确匹配
// 只匹配完全一模一样的 key ['todos',1],不会波及 ['todos',2]、列表分页等其他缓存。
queryClient.invalidateQueries({ queryKey: ['todos', 1], exact: true });
// predicate 自定义规则筛选失效
// 满足条件才失效
queryClient.invalidateQueries({
predicate: (query) => query.queryKey[0] === 'todos' && query.state.data?.length > 10,
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# setQueryData
本地直接修改缓存,不发网络请求
queryClient.setQueryData(['user', userId], (old) => ({
...old,
name: newName,
}));
1
2
3
4
2
3
4
# prefetchQuery
预加载数据(提前缓存)
queryClient.prefetchQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 60 * 1000,
});
1
2
3
4
5
2
3
4
5
# 查询状态
┌──────────┐
│ fresh │ ← staleTime 内
└────┬─────┘
│ staleTime 过期
┌────▼─────┐
┌────→│ stale │ ← 下次使用时后台刷新
│ └────┬─────┘
│ │ 组件卸载(无观察者)
│ ┌────▼─────┐
│ │ inactive │ ← gcTime 倒计时
│ └────┬─────┘
│ │ gcTime 过期
│ ┌────▼─────┐
│ │ deleted │ ← 缓存清除
│ └──────────┘
│
└── refetch / invalidate → 重新获取后回到 fresh
# 时间状态
staleTime(新鲜时间):
数据在多长时间内被认为是"新鲜"的
新鲜 → 不会后台重新获取
过期 → 下次使用时触发后台重新获取
gcTime(垃圾回收时间,原 cacheTime):
查询没有活跃观察者后,缓存在内存中保留多长时间
超时后缓存被删除
时间线示例(staleTime=60s, gcTime=300s):
0s: 组件 A 发起查询 → 请求 → 缓存数据
30s: 组件 B 使用相同 key → 直接返回缓存(数据仍新鲜)
60s: 数据过期(stale)
90s: 组件 C 使用相同 key → 先返回缓存 → 后台重新获取 → 更新
120s: 所有组件卸载(无观察者)
420s: gcTime 到期 → 缓存删除
421s: 新组件查询 → 无缓存 → isLoading=true → 重新请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
上次更新: 2026/07/23, 15:04:03