跳至正文
来两杯美式
返回

React 核心(16):TanStack Query 入门指南

By 来两杯美式
发布于

一、它到底是什么?

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>
  );
}

三个要点:

⚠️ QueryClientuseState(() => 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]),新数据还没到,isPendingtrue → 界面闪烁 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 + isPlaceholderDataplaceholderData 更灵活——除了 keepPreviousData,还可以传函数 (previousData) => previousData 或静态占位数据。

四、useMutation —— 写数据的搭档

useQueryuseMutation(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 QueriesuseInfiniteQuery,无限滚动加载做瀑布流/无限列表时
Parallel QueriesuseQueries,多个查询同时发不阻塞页面同时要多个接口时
Dependent Queriesenabled: !!userId,B 查询等 A 先完成接口有依赖关系时
Optimistic UpdatesonMutate 里先改缓存,失败再回滚追求即时反馈的场景
Suspense 模式useSuspenseQuery,配合 React SuspenseSuspense 机制详解
SSR HydrationHydrationBoundary + dehydrate做 SSR/Next.js 时
Query FiltersinvalidateQueries 的精确匹配模式批量操作缓存时

十一、竞品对比

TanStack QuerySWRRTK 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 观察 → 进阶内容按需学


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
所属专题
React
第 16 / 17 篇
查看系列全部文章
  1. 01.React 核心(1):React 组件与函数的本质区别
  2. 02.React 核心(2):声明式与 React 的实现机制
  3. 03.React 核心(3):Props 完全指南
  4. 04.React 核心(4):useState 完全指南
  5. 05.React 核心(5):useEffect 完全指南
  6. 06.React 核心(6):useRef 完全指南
  7. 07.React 核心(7):Context API 与状态共享
  8. 08.React 核心(8):useCallback 与 useMemo
  9. 09.React 核心(9):Hooks 规则与闭包陷阱
  10. 10.React 核心(10):自定义 Hook 设计
  11. 11.React 核心(11):Key 与列表渲染
  12. 12.React 核心(12):表单与受控/非受控组件
  13. 13.React 核心(13):React 渲染机制深入——Fiber 与调度
  14. 14.React 核心(14):React 18 并发特性
  15. 15.React 核心(15):Suspense 机制详解
  16. 16.React 核心(16):TanStack Query 入门指南
  17. 17.React 核心(17):TanStack Query 缓存机制详解

上一篇
React 核心(17):TanStack Query 缓存机制详解
下一篇
React 核心(15):Suspense 机制详解