Chapter 01

认识 TanStack Query

建立"服务端状态"心智模型,理解为什么 useEffect 手撸数据获取如此痛苦,并搭好第一个 QueryClient。

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 帮你做了什么

能力手撸 useEffectTanStack Query
加载/错误状态手动 3 个 useState内置 isLoading/isError
缓存无(每次重取)自动,按 queryKey
请求去重需自己实现自动合并
后台刷新需自己写聚焦/重连/挂载自动
失败重试手写循环内置指数退避
竞态处理cancelled 标志自动处理
数据同步手动广播invalidateQueries 一行

1.4 从 React Query 到 TanStack Query

这个库最初叫 React Query。v4 起更名为 TanStack Query,因为它把核心逻辑抽成了框架无关的 @tanstack/query-core,再由适配层支持不同框架:

本教程以 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 小结与下一步