核心概念
本章深入 TanStack Query 的核心机制:它到底是什么、查询的生命周期、缓存模型、以及 staleTime 与 gcTime 的本质区别。
TanStack Query 是什么
TanStack Query 是异步状态管理器(Async State Manager),不是数据获取库。它的核心职责:
你提供返回 Promise 的函数
↓
TanStack Query 管理:
✅ 缓存(Cache)—— 数据存在内存中
✅ 去重(Deduplication)—— 相同查询键只发一次请求
✅ 后台更新(Background Refetch)—— 过期数据自动重新获取
✅ 垃圾回收(Garbage Collection)—— 未使用的缓存自动清除
✅ 乐观更新(Optimistic Update)—— 先改 UI,再同步服务器
✅ 重试与错误处理 —— 失败自动重试、错误边界集成🔬 深入原理:TanStack Query 把服务端状态从客户端状态中分离出来。“服务端状态"是一个全新概念——它存储在远端、可以被其他用户修改、可能过期。传统的
useState+useEffect方案无法处理这些特性。
与传统方案的对比
| 场景 | useState + useEffect | TanStack Query |
|---|---|---|
| 请求缓存 | ❌ 需手动管理 | ✅ 自动缓存 + 去重 |
| 后台刷新 | ❌ 需手动实现 | ✅ staleTime + 自动重取 |
| 加载/错误状态 | ❌ 手写 boolean | ✅ isPending / isError 内置 |
| 请求去重 | ❌ 同一数据多次请求 | ✅ 查询键相同则共享 |
| 乐观更新 | ❌ 需大量手写 | ✅ onMutate + setQueryData |
| 分页/无限滚动 | ❌ 需手写逻辑 | ✅ useInfiniteQuery |
| 窗口聚焦刷新 | ❌ 需监听事件 | ✅ refetchOnWindowFocus |
| 离线支持 | ❌ 需从头实现 | ✅ persistQueryClient |
查询的生命周期
一个查询从创建到销毁经历以下阶段:
useQuery 挂载
│
▼
┌──────────────┐
│ fetching │ ← 首次加载,isPending = true
│ (获取中) │
└──────┬───────┘
│ 请求成功
▼
┌──────────────┐
│ fresh │ ← staleTime 内,数据视为新鲜
│ (新鲜) │ 不会自动重新获取
└──────┬───────┘
│ 超过 staleTime
▼
┌──────────────┐
│ stale │ ← 数据已过期,满足触发条件时
│ (过期) │ 后台自动重取
└──────┬───────┘
│ 所有 observer 卸载
▼
┌──────────────┐
│ inactive │ ← 没有组件在使用
│ (非活跃) │
└──────┬───────┘
│ 超过 gcTime
▼
┌──────────────┐
│ garbage │ ← 从缓存中删除
│ collected │
└──────────────┘staleTime vs gcTime
这是 TanStack Query 中最重要也最常被混淆的两个概念:
| 维度 | staleTime | gcTime(v4: cacheTime) |
|---|---|---|
| 含义 | 数据保持"新鲜"的时长 | 缓存从"非活跃"到被删除的时长 |
| 默认值 | 0(立即过期) |
5 * 60 * 1000(5 分钟) |
| 期间行为 | 不触发后台重取 | 缓存驻留内存(卸载后可复用) |
| 触发时机 | observer 挂载 / 窗口聚焦 / 重连 | 最后一个 observer 卸载时开始计时 |
| 典型配置 | 30s ~ 5min | 10min ~ 30min |
时间线示例(staleTime = 30s, gcTime = 5min):
t=0s 首次获取数据,数据变为 fresh
t=30s 数据变为 stale
t=60s 用户切换页面(observer 卸载),缓存进入 inactive
t=90s 用户切回页面(observer 挂载),数据是 stale 的 → 后台重取
t=120s 用户再次离开
t=420s 距离首次 inactive 过去 5min → gcTime 到 → 缓存被回收🔬 深入原理:为什么默认
staleTime: 0?因为大多数应用数据需要实时性——每次组件挂载都立即后台获取。但这不等于每次都显示 loading 状态——如果缓存中有数据,TanStack Query 会先返回缓存,再后台更新(stale-while-revalidate)。
核心三要素
┌─────────────────────────────────────────────┐
│ QueryClientProvider │
│ ┌───────────────────────────────────────┐ │
│ │ QueryClient │ │
│ │ ┌─────────┐ ┌─────────┐ ┌───────┐ │ │
│ │ │ Query │ │ Query │ │ Mutat │ │ │
│ │ │ Cache │ │ Cache │ │ Cache │ │ │
│ │ └─────────┘ └─────────┘ └───────┘ │ │
│ │ ▲ ▲ │ │
│ │ │ │ │ │
│ │ useQuery useQuery useMutation│
│ │ (observer) (observer) (调用) │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────┘QueryClient
全局单例,管理所有查询缓存、默认配置和全局操作。
const queryClient = new QueryClient({
defaultOptions: {
queries: { staleTime: 60_000 },
mutations: {},
},
})
// 全局操作
queryClient.invalidateQueries({ queryKey: ['todos'] })
queryClient.removeQueries({ queryKey: ['todos'] })
queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos })Query(查询)
通过 useQuery 创建的观察者,订阅某个查询键的数据。多个组件使用相同的查询键,共享同一份缓存。
// 这两个组件共享同一份数据和状态
function ComponentA() {
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
}
function ComponentB() {
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
}
// 只发一次网络请求! ✅
Mutation(变更)
通过 useMutation 创建的写操作,用于创建/更新/删除数据。与 Query 不同:不缓存、不自动执行、不共享。
const mutation = useMutation({
mutationFn: createTodo,
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})查询状态机
TanStack Query 的查询有明确的状态转换:
┌─────────────────────┐
│ idle │ ← 尚未 fetch(如 enabled: false)
└──────────┬──────────┘
│ 触发 fetch
▼
┌─────────────────────┐
┌─────│ fetching │──────┐
│ └─────────────────────┘ │
│ 失败 │ 成功
▼ ▼
┌─────────────┐ ┌─────────────────┐
│ error │ │ success │
│ │── retry ───────→│ (data 存在) │
└─────────────┘ └────────┬────────┘
│ 后台 refetch
▼
┌─────────────────┐
│ fetching │
│ (data 仍存在, │
│ isFetching=true) │
└─────────────────┘关键状态字段对照
| 字段 | 含义 | 何时为 true |
|---|---|---|
isPending |
首次加载中,尚无数据 | 没有缓存数据且正在 fetching |
isFetching |
正在获取中 | 任何时候有请求在进行(含后台) |
isLoading |
isPending && isFetching |
v5 新定义:pending 且 fetching |
isError |
请求失败 | error 不为 null |
isSuccess |
请求成功 | data 不为 undefined |
data |
缓存的数据 | isSuccess 时可用 |
error |
错误对象 | isError 时可用 |
// 标准的分支渲染模式
const { data, isPending, isError, error, isFetching } = useQuery({...})
if (isPending) return <Spinner /> // 首次加载:无数据
if (isError) return <Error msg={error.message} />
// data 一定存在
return (
<>
{isFetching && <RefreshingIndicator />} {/* 后台更新 */}
<DataView data={data} />
</>
)Observer(观察者)机制
每个 useQuery 调用都是一个 Observer(观察者)。多个 observer 可以订阅同一个查询键:
- 查询键相同的 observer 共享缓存和请求
- 第一个 observer 挂载时触发首次 fetch
- 最后一个 observer 卸载时,开始 gcTime 倒计时
- 后台重取由配置中的最短 staleTime 决定(所有 observer 中选择最短的刷新间隔)
🔬 深入原理:Observer 模式是 TanStack Query 去重的核心。三个组件同时挂载、都调用
useQuery({ queryKey: ['todos'] })时,只有第一个触发网络请求,其余两个等待同一请求的结果。
v4 → v5 核心概念变化
| 变化 | 原因 |
|---|---|
cacheTime → gcTime |
“垃圾回收"比"缓存时间"更准确地描述行为 |
status: 'loading' → 'pending' |
loading 暗示一定有请求,pendng 仅表示"尚无数据” |
isLoading 含义改变 |
v4: 首次加载;v5: isPending && isFetching(精确描述"加载中”) |
keepPreviousData 移除 |
改为 placeholderData: keepPreviousData(导入函数) |
onSuccess/onError 从 useQuery 移除 |
避免误用;应使用 useEffect 或全局拦截器(QueryCache) |