1.1 一个熟悉的痛点
几乎每个 React 开发者都写过这样的代码:组件里用 useState 存 data、loading、error,用 useEffect 发请求。看起来简单,但真实项目里它会长成这样:
function UserProfile({ id }: { id: number }) {
const [data, setData] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false; // 防竞态
setLoading(true);
fetch(`/api/users/${id}`)
.then(r => r.json())
.then(d => { if (!cancelled) setData(d); })
.catch(e => { if (!cancelled) setError(e); })
.finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; };
}, [id]);
// 还没处理:缓存、去重、后台刷新、聚焦重取、重试……
}
这段代码已经 20 行,却只解决了最基础的问题。如果两个组件都要 user 数据,会发两次请求;切走再切回,又重新 loading 一次;数据在别处被改了,这里毫不知情。这些问题一旦真去解决,你就是在重新发明 TanStack Query。
1.2 关键区分:服务端状态 vs 客户端状态
TanStack Query 的核心洞见是:应用里的"状态"其实有两类,本质完全不同。
- 客户端状态 (Client State)
- 只属于本地、同步、你完全拥有的状态。比如:弹窗是否打开、表单输入值、当前主题、选中的 Tab。它随时能准确读到,改了立刻生效。用
useState/ Zustand / Redux 管理最合适。 - 服务端状态 (Server State)
- 存在远程、通过异步获取、你并不真正"拥有"的状态。比如:用户列表、订单详情、文章内容。它可能在你不知情时被别人改了(过期),需要缓存、去重、后台同步——这正是 TanStack Query 的主场。
- Stale (陈旧)
- 指缓存里的数据"可能已经和服务器不一致"。TanStack Query 用 stale/fresh 状态决定何时后台重新获取,而不是简单地"有缓存就永远用"。
- Cache (缓存)
- 以 queryKey 为索引的内存数据仓库。同一个 key 的数据全应用共享,重复访问直接命中缓存,不再重复请求。
- 去重 (Deduplication)
- 同一时刻多个组件请求同一 queryKey,TanStack Query 只发一次真实请求,结果分发给所有订阅者。
- 后台刷新 (Background Refetch)
- 先把缓存数据立即返回给 UI(不闪 loading),同时在后台悄悄发请求拿最新数据,回来后无缝更新——即 stale-while-revalidate 策略。
- QueryClient
- 整个缓存系统的核心实例,持有 QueryCache 与默认配置。通常整个应用创建一个,通过 Provider 注入。
Redux/Zustand 管"我自己的状态",TanStack Query 管"服务器那份状态的本地副本"。两者不冲突,反而互补——用了 TanStack Query 后,你的全局 store 往往能瘦身一大半。
1.3 TanStack Query 帮你做了什么
| 能力 | 手撸 useEffect | TanStack Query |
|---|---|---|
| 加载/错误状态 | 手动 3 个 useState | 内置 isLoading/isError |
| 缓存 | 无(每次重取) | 自动,按 queryKey |
| 请求去重 | 需自己实现 | 自动合并 |
| 后台刷新 | 需自己写 | 聚焦/重连/挂载自动 |
| 失败重试 | 手写循环 | 内置指数退避 |
| 竞态处理 | cancelled 标志 | 自动处理 |
| 数据同步 | 手动广播 | invalidateQueries 一行 |
1.4 从 React Query 到 TanStack Query
这个库最初叫 React Query。v4 起更名为 TanStack Query,因为它把核心逻辑抽成了框架无关的 @tanstack/query-core,再由适配层支持不同框架:
@tanstack/react-query— React / React Native@tanstack/vue-query— Vue 3@tanstack/solid-query— SolidJS@tanstack/svelte-query— Svelte@tanstack/angular-query— Angular
本教程以 React(v5)为主线讲解,第 9 章会专门介绍其他框架的差异。
1.5 安装
$ npm i @tanstack/react-query
# DevTools(开发调试强烈推荐)
$ npm i -D @tanstack/react-query-devtools
1.6 搭建 QueryClient 与 Provider
在应用根部创建一个 QueryClient,用 QueryClientProvider 把它注入组件树。QueryClient 必须放在组件外或用 useState 惰性创建,否则每次渲染都会新建、丢失缓存。
// main.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 分钟内视为新鲜,不重取
retry: 1, // 失败重试 1 次
},
},
});
createRoot(document.getElementById("root")!).render(
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
1.7 第一个查询:对比前后
还记得 1.1 里 20 行的 UserProfile 吗?用 TanStack Query 重写:
import { useQuery } from "@tanstack/react-query";
function UserProfile({ id }: { id: number }) {
const { data, isLoading, isError } = useQuery({
queryKey: ["user", id],
queryFn: () => fetch(`/api/users/${id}`).then(r => r.json()),
});
if (isLoading) return <p>加载中…</p>;
if (isError) return <p>出错了</p>;
return <h1>{data.name}</h1>;
}
4 行核心逻辑,却白送了缓存、去重、后台刷新、聚焦重取、重试、竞态处理。这就是 TanStack Query 的价值。
TanStack Query 不是 axios 的替代品——它不关心你用 fetch 还是 axios 还是 GraphQL client。它管的是"请求回来的数据怎么缓存、什么时候重取"。queryFn 里返回一个 Promise 即可,用什么发请求随你。
1.8 小结与下一步
- 服务端状态和客户端状态本质不同,需要不同工具管理。
- 手撸 useEffect 只能解决表面问题,缓存/去重/同步会让代码失控。
- TanStack Query = QueryClient(缓存核心)+ useQuery/useMutation(Hook API)。
- 它框架无关,本教程以 React v5 为主线。
- 下一章深入 useQuery——queryKey 设计、queryFn、状态字段、依赖查询。