一、它到底是什么?
TanStack Query(前身 React Query)不是请求库——它不替代 fetch 或 axios。它是服务端状态管理库,管的是请求结果的缓存、状态追踪、自动刷新和多组件数据共享。
类比:fetch/axios 是快递员(负责送货),TanStack Query 是仓库管理员(负责存取和保鲜)。
什么时候该用 / 不该用:
| 场景 | 用不用 | 原因 |
|---|---|---|
| 从后端 API 拿数据展示 | ✅ | 主场 |
| 表单提交 / POST 请求 | ✅ useMutation | 自带重试、错误处理 |
| 多个页面共享同一接口数据 | ✅ | 缓存复用,自动刷新 |
| 纯前端状态(主题、modal 开关) | ❌ | 用 useState / Zustand |
| 一次性只读配置(不变化) | ⚠️ | 简单场景 fetch + useState 就够 |
| 实时数据(WebSocket) | ⚠️ | 配合 queryClient.setQueryData 手动更新缓存 |
二、最小可运行骨架
import {
QueryClient,
QueryClientProvider,
useQuery,
} from "@tanstack/react-query";
// ① 创建 client —— 每个组件树生命周期一个实例
// 用 useState 确保单例,同时兼容 SSR(模块顶层单例会导致用户间数据污染)
function App() {
const [queryClient] = useState(() => new QueryClient());
// ② 包裹应用
return (
<QueryClientProvider client={queryClient}>
<TodoList />
</QueryClientProvider>
);
}
// ③ 在组件里用 useQuery 发请求
function TodoList() {
const { data, isPending, error } = useQuery({
queryKey: ["todos"], // 缓存键
queryFn: getTodos, // 返回 Promise 的函数
});
if (isPending) return "Loading...";
if (error) return "Error: " + error.message;
return (
<ul>
{data.map(t => (
<li key={t.id}>{t.title}</li>
))}
</ul>
);
}
三个要点:
- queryKey:缓存标识,相同 key 共享同一份数据
- queryFn:返回 Promise 的函数,里面用 fetch/axios 都行
- 返回值:
data(数据)、isPending(首次加载中)、error(错误)
⚠️
QueryClient用useState(() => new QueryClient())创建,别写在模块顶层——SPA 看起来没问题,但 SSR 时模块顶层单例会导致所有用户共享同一份缓存,造成数据泄漏。useState的惰性初始化确保每个组件树有独立实例,同时不会因重渲染而重建。
三、useQuery 详解
3.1 常用返回值
const {
data, // 请求成功后的数据(未返回时是 undefined)
error, // 错误对象
isPending, // 首次加载中(还没有任何数据)
isFetching, // 正在请求中(包括后台刷新)
isError, // 是否出错
isSuccess, // 是否成功
dataUpdatedAt, // 数据更新时间戳
refetch, // 手动触发重新请求
} = useQuery({ queryKey: ["todos"], queryFn: getTodos });
3.2 返回值是动态的:每次渲染都读,状态变了自动重渲染
useQuery 不是”调用一次拿一个结果”,而是每次组件渲染时读取当前状态。当查询内部状态变化时,React 自动触发组件重新渲染,你就拿到了新的返回值。
① 组件挂载,发起请求
→ { isPending: true, data: undefined } → 渲染 <Spinner />
② 请求成功(1.5s 后)
→ { isPending: false, data: [...] } → 渲染 <List />
③ 用户切走又切回,数据已 stale,触发后台 refetch
→ { isPending: false, isFetching: true, data: [...] }
→ 渲染 <List />(data 还在,用户无感知)
④ 后台 refetch 完成,数据有更新
→ { isPending: false, isFetching: false, data: [...新数据] }
→ 渲染 <List />(内容静默替换)
关键:
data在后台 refetch 期间不会变成undefined——它保持上一次的值,直到新数据到了才替换。这就是 SWR 体验的来源:用户永远看到内容,不会闪白屏。
所以你写的 if (isPending) return <Spinner /> 本质是声明式的——告诉 React “isPending 为真时画这个”,React 会在返回值变化时自动替你重新判断。
3.3 Loading 状态的精细区分
| 状态 | 含义 | 场景 |
|---|---|---|
isPending | 还没有任何数据 | 第一次请求中 |
isFetching | 正在请求中 | 第一次请求 或 后台刷新 |
isError | 请求失败 | - |
v5 中
isLoading=isPending && isFetching,官方推荐用isPending。
推荐模式——有缓存数据时后台刷新,不切回 loading:
if (isPending) return <Spinner />; // 第一次才显示骨架屏
if (isError) return <Error error={error} />;
// 有数据时直接展示,后台刷新用户无感知
return <List data={data} />;
3.4 常用选项
useQuery({
queryKey: ["user", userId],
queryFn: () => fetchUser(userId),
enabled: !!userId, // 条件查询:userId 存在才发请求
staleTime: 5 * 60 * 1000, // 5 分钟内认为数据新鲜,不重新请求
gcTime: 30 * 60 * 1000, // 30 分钟无引用后垃圾回收
retry: 3, // 失败重试 3 次
refetchOnWindowFocus: false, // 禁止窗口获焦时自动刷新
});
enabled是依赖查询的关键:当 B 查询依赖 A 查询的结果时,enabled: !!aData让 B 等 A 完成后再请求。
3.5 翻页不闪烁:placeholderData
分页场景中,切换页码时 queryKey 变了(如 ['projects', 1] → ['projects', 2]),新数据还没到,isPending 变 true → 界面闪烁 Loading 骨架屏。placeholderData 让你在等新数据时继续展示旧数据:
import { keepPreviousData, useQuery } from "@tanstack/react-query";
const { data, isPlaceholderData } = useQuery({
queryKey: ["projects", page],
queryFn: () => fetchProjects(page),
placeholderData: keepPreviousData, // v5 写法(旧版 keepPreviousData: true 已废弃)
});
效果:翻页时 data 先保持上一页的内容(isPlaceholderData: true),新数据到了无缝替换。用户看到”旧内容 → 新内容”,而非”旧内容 → 闪烁 loading → 新内容”。
v5 变更:
keepPreviousData: true+isPreviousData已废弃,改为placeholderData: keepPreviousData+isPlaceholderData。placeholderData更灵活——除了keepPreviousData,还可以传函数(previousData) => previousData或静态占位数据。
四、useMutation —— 写数据的搭档
useQuery 管读,useMutation 管写(POST / PUT / DELETE):
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: postTodo,
onSuccess: () => {
// 写成功后,让 todos 缓存失效 → 自动重新请求
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
// 触发
mutation.mutate({ title: "New Todo" });
4.1 完整回调链
const mutation = useMutation({
mutationFn: updatePost,
onMutate: async variables => {
// 1. mutation 执行前 —— 乐观更新、取消正在进行的 refetch
await queryClient.cancelQueries({ queryKey: ["posts", variables.id] });
const previous = queryClient.getQueryData(["posts", variables.id]);
queryClient.setQueryData(["posts", variables.id], variables);
return { previous }; // 传给 onError / onSettled
},
onSuccess: (data, variables) => {
// 2. 成功 —— 失效相关查询
queryClient.invalidateQueries({ queryKey: ["posts"] });
},
onError: (error, variables, context) => {
// 3. 失败 —— 回滚乐观更新
if (context?.previous) {
queryClient.setQueryData(["posts", variables.id], context.previous);
}
},
onSettled: () => {
// 4. 无论成功失败 —— 确保与服务器同步
queryClient.invalidateQueries({ queryKey: ["posts"] });
},
});
4.2 mutate vs mutateAsync
mutation.mutate(data); // 不返回 Promise,错误走 onError 回调
await mutation.mutateAsync(data); // 返回 Promise,可用 try/catch
五、Query Key 设计
Query Key 不只是字符串,是层级数组,越具体越精确:
[
"todos",
] // 所有 todos
[
("todos", "done")
] // 已完成的 todos
[
("todos", { id: 1 })
] // id=1 的 todo
[("users", 1, "todos")]; // 用户 1 的 todos
invalidateQueries 是前缀匹配——invalidateQueries({ queryKey: ['todos'] }) 会让所有以 todos 开头的缓存都失效。
Query Key Factory 模式(推荐)
集中管理所有 key,避免到处硬编码:
// queryKeys.ts
export const queryKeys = {
users: {
all: ["users"] as const,
lists: () => [...queryKeys.users.all, "list"] as const,
list: (filters: Record<string, string>) =>
[...queryKeys.users.lists(), filters] as const,
detail: (id: number) => [...queryKeys.users.all, id] as const,
},
} as const;
// 组件里
useQuery({
queryKey: queryKeys.users.detail(userId),
queryFn: () => fetchUser(userId),
});
// 失效时一键刷掉所有 users 相关缓存
queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
六、staleTime vs gcTime —— 两个时间轴
初学者最容易搞混的概念:
| staleTime(新鲜度) | gcTime(垃圾回收时间) | |
|---|---|---|
| 默认值 | 0(立即过期) | 5 分钟 |
| 管什么 | 数据”还新不新” → 新的不重新请求 | 数据”还留不留” → 没人用的缓存何时删除 |
| 比喻 | 菜还热不热,热的不重做 | 隔夜菜丢不丢,多久没人吃就倒掉 |
生命周期时间线
请求成功 → 数据进入缓存(fresh)
│
staleTime 到期 → 数据变为 stale(但还在缓存里!)
│ 此时会触发后台静默刷新
│
组件卸载,没人用这个数据了
│
gcTime 到期 → 数据从缓存删除(彻底消失)
为什么默认 staleTime=0?
TanStack Query 的哲学是”服务端数据默认就不新鲜”——你永远不知道服务端什么时候更新了数据,所以每次组件挂载或窗口获焦都后台刷一次,保证数据尽可能新。
按场景选择 staleTime
| 场景 | 建议值 |
|---|---|
| 用户资料(很少变) | 5-10 分钟 |
| 仪表盘(频繁变) | 30 秒 - 1 分钟 |
| 实时数据(股票、聊天) | 0 或用 refetchInterval |
| 静态配置 | Infinity |
七、自动刷新的三个触发时机
TanStack Query 默认在以下时机自动后台刷新(只限 stale 数据):
| 时机 | 选项 | 默认 |
|---|---|---|
| 组件挂载 | — | ✅ 开启 |
| 窗口获焦 | refetchOnWindowFocus | ✅ 开启 |
| 网络重连 | refetchOnReconnect | ✅ 开启 |
开发环境下 React StrictMode 会导致组件 mount 两次,可能看到请求发了两次——这是正常行为,生产环境不会。
八、DevTools —— 调试神器
npm i @tanstack/react-query-devtools
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourComponent />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
DevTools 可以可视化查看所有缓存的 query、状态(fresh/stale/inactive)、数据内容、时间线。建议一开始就装上,对理解缓存行为帮助极大。
九、常见陷阱
9.1 mutation 后忘了 invalidate
最常见错误——写了数据但界面没更新:
// ❌ mutation 成功了,UI 没反应
const mutation = useMutation({ mutationFn: createPost });
// ✅ 成功后让相关缓存失效
const mutation = useMutation({
mutationFn: createPost,
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["posts"] }),
});
9.2 Query Key 没包含动态参数
// ❌ 所有 userId 共用同一个缓存
useQuery({ queryKey: ["user"], queryFn: () => fetchUser(userId) });
// ✅ 每个 userId 独立缓存
useQuery({ queryKey: ["user", userId], queryFn: () => fetchUser(userId) });
9.3 queryFn 不处理错误
queryFn 返回的 Promise reject 了才会触发 isError。用 fetch 时别忘了检查响应:
// ❌ 404/500 不会触发 isError(fetch 不对 HTTP 错误 reject)
queryFn: () => fetch("/api/user").then(res => res.json());
// ✅ 手动抛出错误
queryFn: async () => {
const res = await fetch("/api/user");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
};
9.4 把 query data 复制到 useState
// ❌ 背景 refetch 后,state 和缓存脱节
const { data } = useQuery({ queryKey: ["user"], queryFn: fetchUser });
const [user, setUser] = useState(data);
// ✅ 直接用 data,或用 useMemo 派生
const { data } = useQuery({ queryKey: ["user"], queryFn: fetchUser });
const displayName = useMemo(() => data?.name ?? "", [data]);
9.5 QueryClient 创建方式
// ❌ 模块顶层单例:SSR 时所有用户共享缓存 → 数据泄漏
const queryClient = new QueryClient();
function App() {
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>;
}
// ❌ 组件内直接创建:每次渲染都新建 → 缓存全丢
function App() {
const queryClient = new QueryClient();
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>;
}
// ✅ useState 惰性初始化:单例 + 兼容 SSR
function App() {
const [queryClient] = useState(() => new QueryClient());
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>;
}
Next.js App Router 的更精细写法:服务端每次请求新建 client,浏览器端复用单例——见 TanStack Query 缓存机制 的 SSR 配置章节。
9.6 缺少 enabled 守卫
// ❌ userId 为 undefined 时,仍会创建一个 ['user', undefined] 的缓存条目
// 状态为 pending + fetchStatus: idle(不发请求,但占用缓存空间)
useQuery({ queryKey: ["user", userId], queryFn: () => fetchUser(userId) });
// ✅ enabled: false 时完全阻止请求,缓存条目为 pending/idle,不浪费网络资源
useQuery({
queryKey: ["user", userId],
queryFn: () => fetchUser(userId),
enabled: !!userId,
});
注意:
enabled: false并非什么都不做——它仍会在缓存中注册一个status: 'pending'、fetchStatus: 'idle'的条目。这不会触发网络请求,但如果 key 中包含undefined(如['user', undefined]),TanStack Query 会将其哈希为['user', null]——虽然不会出错,但语义不正确且占用缓存空间。实践中问题不大,但养成加enabled的习惯是好的。
十、进阶路线图
以下内容等实际需求出现再学:
| 主题 | 一句话说明 | 何时学 |
|---|---|---|
| Prefetching | 提前请求下一页数据,用户点过去秒开 | 做列表/分页时 |
| Infinite Queries | useInfiniteQuery,无限滚动加载 | 做瀑布流/无限列表时 |
| Parallel Queries | useQueries,多个查询同时发不阻塞 | 页面同时要多个接口时 |
| Dependent Queries | enabled: !!userId,B 查询等 A 先完成 | 接口有依赖关系时 |
| Optimistic Updates | onMutate 里先改缓存,失败再回滚 | 追求即时反馈的场景 |
| Suspense 模式 | useSuspenseQuery,配合 React Suspense | 见 Suspense 机制详解 |
| SSR Hydration | HydrationBoundary + dehydrate | 做 SSR/Next.js 时 |
| Query Filters | invalidateQueries 的精确匹配模式 | 批量操作缓存时 |
十一、竞品对比
| TanStack Query | SWR | RTK Query | |
|---|---|---|---|
| 包大小 | ~13KB | ~4KB | ~9KB(+Redux) |
| 学习曲线 | 中等 | 低 | 中高 |
| 最适合 | 复杂 CRUD、infinite scroll、多框架 | 简单读取、Next.js、极致 bundle | 已用 Redux 的项目 |
| DevTools | ⭐⭐⭐ | ⭐ | ⭐⭐(Redux DevTools) |
| Optimistic Update | 原生支持 | 手动 | 手动 |
| 框架支持 | React/Vue/Svelte/Solid | 仅 React | 仅 React |
React 入门选 TanStack Query——生态最大、文档最全、DevTools 最强。
推荐学习路径:搭骨架 → useQuery 读数据 → useMutation 写数据 → Key 设计 → staleTime/gcTime 调优 → 装上 DevTools 观察 → 进阶内容按需学