首页 / TanStack 生态入门教程 / 查询状态详解

TanStack 生态入门教程

查询状态详解

本教程共 38 篇 · 第 9 篇 · 更新于 2026-07-27 · 约 9 分钟阅读

TanStackTanStack 生态入门教程statusfetchStatusisPendingisLoadingisFetchingQueryFilters

9. 查询状态详解

本节目标:彻底分清 status 和 fetchStatus、isPending 和 isLoading 的区别,学会用派生状态简化渲染,掌握 QueryFilters 的过滤用法,学完再也不会被各种状态绕晕。

9.1 两个状态维度

Query 的状态有两个维度,这是最容易绕晕的地方:

  • status:描述数据,「有没有数据」
  • fetchStatus:描述请求,「queryFn 在不在跑」

它们是正交的,互相独立。一个查询可以同时在 success 状态(有数据)和 fetching 状态(后台正在刷新)。

status 的三个值

const { status } = useQuery({...})

// status 只能是这三个之一:
// 'pending'  -- 还没有数据
// 'error'    -- 出错了
// 'success'  -- 成功,有数据

对应的布尔快捷标志:

  • isPending = status === ‘pending’
  • isError = status === ‘error’
  • isSuccess = status === ‘success’

fetchStatus 的三个值

const { fetchStatus } = useQuery({...})

// fetchStatus 只能是这三个之一:
// 'fetching'  -- 正在请求
// 'paused'    -- 想请求但被暂停(比如离线)
// 'idle'      -- 没在干啥

isFetching 就是 fetchStatus === 'fetching' 的快捷写法。

9.2 为什么要有两个维度

后台刷新和 stale-while-revalidate 逻辑让 status 和 fetchStatus 的组合变得多样:

  • 一个 success 状态的查询,通常 fetchStatusidle。但如果正在后台刷新,fetchStatus 就是 fetching
  • 一个刚挂载的查询,通常是 pending + fetching。但如果网络断了,可能是 pending + paused

关键认知:

  • status 告诉你数据层面的情况:有还是没有
  • fetchStatus 告诉你请求层面的情况:queryFn 在跑还是没有

一个查询可以 pending 但没在 fetching。比如 enabled: false 的懒查询,挂载时是 pending + idle—没数据,但也没在请求。

9.3 isPending vs isLoading

v5 改了 isLoading 的语义,这是个大坑。

v5 之前:

  • isLoading = 首次加载(没数据且在请求)

v5 之后:

  • isPending = 没数据(status === ‘pending’)
  • isLoading = isPending && isFetching(没数据且正在请求)

也就是说,isLoading 在 v5 里变成了派生状态,只在「首次加载、正在请求」时为 true。

场景isPendingisFetchingisLoading
首次加载中truetruetrue
懒查询未启用truefalsefalse
有数据后台刷新falsetruefalse
有数据空闲falsefalsefalse
Warning

老代码从 v4 升 v5,如果你之前用 isLoading 判断「没数据」,v5 里要改成 isPendingisLoading 在 v5 里语义变了,懒查询场景下 isLoading 是 false 但 isPending 是 true。v4 的 isInitialLoading 在 v5 里等于新的 isLoading,已废弃。

9.4 什么时候用哪个

日常渲染,用 isPending 就够了:

const { isPending, isError, data, error } = useQuery({...})

if (isPending) return <span>加载中...</span>
if (isError) return <span>出错了:{error.message}</span>
// 默认 success
return <div>{data.map(...)}</div>

懒查询场景,用 isLoading 更准:

const { isLoading, data } = useQuery({
  queryKey: ['todos', filter],
  queryFn: () => fetchTodos(filter),
  enabled: !!filter,
})

// filter 为空时 isPending 是 true 但 isLoading 是 false
// filter 有值且首次请求时 isLoading 才是 true
if (isLoading) return <span>加载中...</span>

想显示「后台刷新中」的提示,用 isFetching

const { status, data, isFetching } = useQuery({...})

return (
  <>
    {isFetching && <div>刷新中...</div>}
    {status === 'success' && <div>{data}</div>}
  </>
)

9.5 后台刷新指示器

isFetching 是单个查询的。如果想显示全局的后台刷新指示(任意查询在跑都显示),用 useIsFetching

import { useIsFetching } from '@tanstack/react-query'

function GlobalLoadingIndicator() {
  const isFetching = useIsFetching()

  return isFetching ? (
    <div className="global-spinner">有查询在后台刷新...</div>
  ) : null
}

useIsFetching() 返回当前正在 fetching 的查询数量,0 就是都没在跑。可以放页面顶部,用户能看到「应用正在后台更新数据」。

还能按 key 过滤:

// 只统计 todos 相关的查询
const fetchingTodosCount = useIsFetching({ queryKey: ['todos'] })

9.6 派生状态:select

有时候后端返回的数据结构和你想用的不一样,可以用 select 派生:

const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  select: (data) => data.filter((todo) => todo.done),
})
// data 是已过滤的「已完成」todo 列表

select 会在数据到达时执行,结果作为 data 返回。好处是:

  • 类型会跟着推导(前面讲过)
  • 配合结构共享,如果 select 的结果没变,引用不变,不会触发无谓重渲染
// 复杂转换也行
const { data: doneCount } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  select: (todos) => todos.filter((t) => t.done).length,
})
// data 是数字:已完成数量
Tip

select 适合做视图层的数据转换(过滤、排序、取部分字段)。如果转换很重,考虑用 useMemo 缓存,或者干脆在 queryFn 里就转好。

9.7 Query 派生状态 vs 客户端状态

新手常问:Query 是不是要替代 Redux、Zustand 这些状态管理库?

答案:不替代,但能大幅减少全局状态的量

Query 管的是服务端状态—从服务器拿来的数据。Redux 这些管的是客户端状态—UI 状态、用户偏好这类。

一个典型项目的全局状态可能是:

const globalState = {
  projects,    // 服务端数据
  teams,       // 服务端数据
  tasks,       // 服务端数据
  users,       // 服务端数据
  themeMode,   // 客户端状态
  sidebarOpen, // 客户端状态
}

把服务端数据挪到 Query 之后,剩下的全局状态往往就剩这点:

const globalState = {
  themeMode,
  sidebarOpen,
}

这点状态用 useState 传 props、或者用 Context、或者用 Store 都行,没必要上 Redux。

Note

也有例外:如果你的应用有大量同步的客户端状态(比如可视化编辑器、音乐制作软件),还是需要专门的状态管理库。Query 和它们能共存,不冲突。

9.8 QueryFilters:过滤查询

有些 API 能批量操作查询,比如 invalidateQueriesrefetchQueriesremoveQueries。它们都接受一个 QueryFilters 对象来筛选要操作哪些查询。

// 失效所有查询
queryClient.invalidateQueries()

// 失效所有以 'todos' 开头的查询
queryClient.invalidateQueries({ queryKey: ['todos'] })

// 刷新所有 active 查询
await queryClient.refetchQueries({ type: 'active' })

// 移除所有 inactive 且以 'posts' 开头的查询
queryClient.removeQueries({ queryKey: ['posts'], type: 'inactive' })

QueryFilters 支持的过滤条件:

  • queryKey — 按 key 前缀匹配(默认不是精确匹配)
  • exact: true — 改成精确匹配
  • type: 'active' | 'inactive' | 'all' — 按活跃状态过滤,默认 all
  • stale: boolean — true 匹配 stale 的,false 匹配 fresh 的
  • fetchStatus — 按 fetching 状态过滤
  • predicate: (query) => boolean — 自定义判断函数

精确匹配 vs 前缀匹配

默认是前缀匹配,这和 QueryKey 的层级设计配合:

// 前缀匹配:所有以 'todos' 开头的都命中
queryClient.invalidateQueries({ queryKey: ['todos'] })
// 命中 ['todos']、['todos', 5]、['todos', { type: 'done' }] 等

// 精确匹配:只命中完全相等的
queryClient.invalidateQueries({ queryKey: ['todos'], exact: true })
// 只命中 ['todos'],不命中 ['todos', 5]

predicate 自定义过滤

更复杂的条件用 predicate

queryClient.invalidateQueries({
  predicate: (query) =>
    query.queryKey[0] === 'todos' && query.queryKey[1]?.version >= 10,
})

这会失效所有第一项是 'todos'、第二项的 version 大于等于 10 的查询。灵活但别滥用,predicate 会对每个缓存查询执行一遍,多了影响性能。

9.9 MutationFilters

mutation 也有过滤器,字段类似但少了几个:

// 统计正在进行的 mutation 数量
const mutatingCount = useIsMutating()

// 按 key 过滤
const mutatingTodos = useIsMutating({ mutationKey: ['addTodo'] })

// 自定义判断
useIsMutating({
  predicate: (mutation) => mutation.state.variables?.id === 1,
})

9.10 小结

状态这块记住核心:

  • status 管数据有没有,fetchStatus 管请求在不在跑
  • isPending = 没数据,isLoading = 没数据且在请求(v5 新语义)
  • 懒查询用 isLoading,普通查询用 isPending
  • isFetching 显示后台刷新,useIsFetching 显示全局刷新
  • select 做视图层数据转换
  • QueryFilters 按前缀、精确、predicate 过滤查询

下一篇进入 Mutation,开始学怎么改数据。