React 进阶← 返回文章列表
TanStack Query (React Query) 架构与性能优化实战:请求去重、缓存分层与数据预取
厘清客户端 UI 状态与服务端缓存的区别,通过 QueryKey 去重、staleTime/gcTime 配置与乐观更新提升数据加载体验。
LJ
· 9 分钟
本页目录展开 / 收起
读完你能做什么
- 区分请求函数、服务端状态缓存与客户端 UI 状态。
- 为 Query Key、
staleTime和gcTime建立可预测的规则。 - 安全实现预取、失效与乐观更新,并在失败时回滚。
Axios 负责“怎样发请求”,TanStack Query 负责“何时请求、结果缓存在哪里、何时视为过期”。两者并不竞争,职责边界清楚后配置才有意义。
在 React 里用 useEffect 发请求,最常见的问题是样板代码满天飞:每个接口都要自己维护 isLoading、isError 和 data 三个状态。组件一多,兄弟组件重复发起相同的接口请求,或者切个 Tab 回来页面又白屏闪一下重新拉数据。TanStack Query 解决的核心痛点,就是把“数据发请求、缓存多久、什么时候后台刷新”当成一套独立的服务端缓存来管。
Axios vs TanStack Query:职责边界划分
┌──────────────────────────────────────────────────────────┐
│ TanStack Query (服务端状态中枢) │
│ ├── 智能内存缓存 (QueryCache) & 垃圾回收 (gcTime) │
│ ├── 请求合并与去重 (Request Deduplication) │
│ ├── 窗口聚焦 / 网络恢复自动静默刷新 (Refetch on Focus) │
│ └── 乐观更新 (Optimistic UI) & 预取 (Prefetching) │
├──────────────────────────────────────────────────────────┤
│ Axios / Fetch (底层 HTTP 通信管道) │
│ └── 仅负责发送 HTTP 报文、携带 Headers、处理拦截器 │
└──────────────────────────────────────────────────────────┘[!NOTE] React Query 不是 Axios 的替代品,而是它的管理者。React Query 负责调度“何时发起请求、何时复用缓存”,而具体的数据获取依然通过 Axios 或
fetch执行。
核心优化一:QueryKey 与并发请求自动去重(Deduplication)
在复杂仪表盘中,导航栏头像组件、侧边栏用户徽章组件和主内容区往往同时需要当前登录用户的信息:
组件 A (NavBar) ─── useQuery(['user', 'me']) ──┐
组件 B (SideBar) ─── useQuery(['user', 'me']) ──┼──► 仅发出 1 次 HTTP 请求!
组件 C (MainPanel) ─── useQuery(['user', 'me']) ──┘ (其余组件共享同一 In-Flight Promise)只要 queryKey 严格一致,TanStack Query 会在微任务级别自动拦截并合并所有并发的相同请求,避免后端被无意义的重复流量冲击。
核心优化二:理解 staleTime 与 gcTime
很多开发者对 React Query 数据的“保鲜期”产生误解:
数据生命周期时序:
[数据获取成功] ────(staleTime: 2分钟)────► [数据变为 Stale 陈旧] ────(gcTime: 10分钟)────► [内存回收]
│ │
▼ ▼
在此期间重新挂载组件: 在此期间重新挂载组件:
直接读取内存缓存,不发起任何网络请求 先展示缓存(0等待),同时后台静默发请求刷新staleTime(数据保鲜期):默认是0。在staleTime内,组件重新渲染或再次挂载直接使用缓存,绝不发请求;gcTime(垃圾回收时间,v4 称 cacheTime):默认是5 分钟。当没有任何组件在使用这个 query 时,数据会在内存中保留指定时间后被清除。
推荐全局配置:
// lib/query-client.ts
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 2, // 2 分钟内数据视为最新,避免频繁抖动
gcTime: 1000 * 60 * 10, // 闲置 10 分钟后释放内存
refetchOnWindowFocus: false, // 视业务需求决定切屏是否刷新
retry: 2, // 失败重试 2 次
},
},
});核心优化三:乐观更新(Optimistic Updates)提升操作手感
在点赞、收藏或状态切换等高频操作中,等待后端返回 200 再更新 UI 会给用户带来滞后感。
利用 useMutation 的 onMutate,我们可以在请求发出瞬间直接修改本地缓存:
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newTodo: { text: string }) => axios.post('/api/todos', newTodo),
// 1. 在请求发送前立即触发
onMutate: async (newTodo) => {
// 取消相关的在途查询,防止覆盖乐观数据
await queryClient.cancelQueries({ queryKey: ['todos'] });
// 保存先前的快照用于回滚
const previousTodos = queryClient.getQueryData(['todos']);
// 乐观地直接更新本地缓存
queryClient.setQueryData(['todos'], (old: any) => [...old, { id: 'temp-id', ...newTodo }]);
return { previousTodos };
},
// 2. 若接口报错,秒级回滚到旧快照
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context?.previousTodos);
toast.error('添加失败,已回滚');
},
// 3. 无论成功失败,最后重新与服务端同步一次
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});官方资料
最容易踩的两个坑
- staleTime 默认值为 0:这是新手最困惑的地方。默认情况下,只要组件重新挂载或窗口切回前台,TanStack Query 就会立刻在后台重新发请求(即使两秒前刚拉过数据)。对于不常变动的基础数据,一定要显式配置合理的
staleTime(例如 1~5 分钟)。 - QueryKey 数组深度比较:QueryKey 内部采用哈希校验。对象参数中的键值顺序不同也会被判为相同(
{ a: 1, b: 2 }与{ b: 2, a: 1 }视为等价),但传未固定的动态引用或非序列化数据会导致缓存频繁击穿。