查询状态详解
本教程共 38 篇 · 第 9 篇 · 更新于 2026-07-27 · 约 9 分钟阅读
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状态的查询,通常fetchStatus是idle。但如果正在后台刷新,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。
| 场景 | isPending | isFetching | isLoading |
|---|---|---|---|
| 首次加载中 | true | true | true |
| 懒查询未启用 | true | false | false |
| 有数据后台刷新 | false | true | false |
| 有数据空闲 | false | false | false |
Warning老代码从 v4 升 v5,如果你之前用
isLoading判断「没数据」,v5 里要改成isPending。isLoading在 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 能批量操作查询,比如 invalidateQueries、refetchQueries、removeQueries。它们都接受一个 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'— 按活跃状态过滤,默认 allstale: 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,开始学怎么改数据。