Skip to content
核心概念

核心概念

本章深入 TanStack Query 的核心机制:它到底是什么、查询的生命周期、缓存模型、以及 staleTimegcTime 的本质区别。


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 核心概念变化

变化 原因
cacheTimegcTime “垃圾回收"比"缓存时间"更准确地描述行为
status: 'loading''pending' loading 暗示一定有请求,pendng 仅表示"尚无数据”
isLoading 含义改变 v4: 首次加载;v5: isPending && isFetching(精确描述"加载中”)
keepPreviousData 移除 改为 placeholderData: keepPreviousData(导入函数)
onSuccess/onError 从 useQuery 移除 避免误用;应使用 useEffect 或全局拦截器(QueryCache)