TanStack Query(Preact Query)useIsMutating Hook 全面解析:统计进行中的 Mutation 并构建全局加载指示器
导读
useIsMutating 是 @tanstack/preact-query 提供的一个可选 Hook,用于返回应用中当前正处于 pending(进行中/fetching) 状态的 mutation 数量。它最常见的应用场景是渲染“保存中 / 提交中……”之类的应用级全局加载指示器——当页面同时存在多个表单或按钮触发 mutation 时,任何变更尚未落定都会让这个数值大于 0。阅读本文后,你将掌握该 Hook 的完整类型签名、过滤参数的含义与默认行为、在 Preact 应用中的接入方式,以及其底层依赖 MutationCache、useMutationState 的计数与订阅原理,并可通过仓库内的单元测试验证这些行为。
本文以 useIsMutating.md 为主干,结合 preact-query 与 query-core 源码展开。
类型签名与核心作用
在 preact-query/src/useMutationState.ts:35 中,该 Hook 的类型签名如下:
function useIsMutating(filters?, queryClient?): number;
其语义为:返回你的应用中正在“fetching”(处于 pending 状态)的 mutation 数量。这里的数量是跨所有组件共享的——无论 mutation 是在哪个组件内通过 useMutation 触发的,只要它们位于同一个 QueryClient 的 MutationCache 中,就会被统一计数,因此它特别适合做应用级的加载指示器,而不仅是某个按钮局部的 loading 状态。
需要特别说明的是:“fetching”这一表述在源码层面对应的是 mutation 的 status === 'pending'。也就是说,这个数字反映的是正在执行(尚未 resolve/reject)的 mutation 数量,而不是处于 idle、success 或 error 状态的数量。
参数详解
filters?(可选)
类型为 MutationFilters<unknown, Error, unknown, unknown>。
用于缩小被统计的 mutation 范围。MutationFilters 完整接口定义在 packages/query-core/src/utils.ts:54,包含四个可选字段:
| 字段 | 类型 | 作用说明 |
|---|---|---|
exact |
boolean |
是否要求 mutation key 与给定的 key 完全相等。默认 false,即默认采用前缀匹配 |
predicate |
(mutation) => boolean |
传入一个判定函数,对每个候选 mutation 进行任意自定义筛选 |
mutationKey |
MutationKey 或其元组前缀 |
只统计 mutation key 能匹配给定 key 的 mutation |
status |
MutationStatus |
按状态过滤,MutationStatus 取值为 'idle' | 'pending' | 'success' | 'error'(见 packages/query-core/src/types.ts:1078) |
useIsMutating 内部会把你传入的 filters 再强制叠加一层 status: 'pending' 过滤(稍后源码一节会证明),所以即使你手动传入了 status,最终也只统计进行中的 mutation。
queryClient?(可选)
类型为 QueryClient。
用于显式指定一个自定义 QueryClient。若不传,则使用最近上下文(即最靠近当前组件的 QueryClientProvider)中提供的实例。关于 Provider 的接入方式可参考 QueryClientProvider.md 与 useQueryClient.md。
返回值
返回类型为 number,即当前处于 pending 状态的 mutation 数量。组件会随数量变化而响应式重渲染;当没有匹配的 mutation 时返回 0,因此可直接用真值判断驱动 UI。
官方示例:按 mutationKey 前缀统计
下面是文档中给出的完整示例——统计所有 key 前缀命中 ['posts'] 的、正在进行的 mutation:
import { useIsMutating } from '@tanstack/preact-query'
function PostsMutatingIndicator() {
// How many mutations matching the posts prefix are in progress?
const isMutatingPosts = useIsMutating({ mutationKey: ['posts'] })
return isMutatingPosts ? <span>Saving posts...</span> : null
}
这个写法建立在默认前缀匹配规则之上:任何通过 useMutation({ mutationKey: ['posts', ...] }) 或 mutationKeyFn 产生 ['posts', id] 这类 key 的 mutation,只要处于 pending 状态,都会让 isMutatingPosts 加 1。
底层实现原理:useMutationState + MutationCache
一层薄封装:强制 pending 过滤并取数组长度
useIsMutating 的实现本身非常精简——它就是对 useMutationState 的一层封装。源码位于 packages/preact-query/src/useMutationState.ts:35:
export function useIsMutating(
filters?: MutationFilters,
queryClient?: QueryClient,
): number {
const client = useQueryClient(queryClient)
return useMutationState(
{ filters: { ...filters, status: 'pending' } },
client,
).length
}
从源码可以清晰地看到两个关键事实:
{ ...filters, status: 'pending' }:传入的过滤条件被展开后,强行覆盖status为'pending',确保useIsMutating的返回值只代表“进行中”的 mutation 数量。- 取返回数组的
.length:useMutationState返回与过滤条件匹配的 mutation 状态数组,useIsMutating直接取其长度。
两者在 packages/preact-query/src/index.ts:52 中一并作为公开 API 从包入口导出。
MutationCache 的匹配与订阅机制
useMutationState 的完整实现位于同一文件 useMutationState.ts:157,其数据来源是 QueryClient 上的 MutationCache:
const mutationCache = useQueryClient(queryClient).getMutationCache()
初次渲染时通过 getResult(mutationCache, options) 立刻计算出结果快照,其内部调用 mutationCache.findAll(options.filters)(对应 packages/query-core/src/mutationCache.ts:219)并逐条映射出 mutation 状态;随后通过 mutationCache.subscribe(...) 监听缓存变更,并借助 replaceEqualDeep 做深比较,仅在结果实际变化时才通过 notifyManager.schedule 调度重渲染。最终由 useSyncExternalStore 把订阅桥接到 Preact 的响应式渲染周期。也就是说,一旦任何 mutation 状态翻转(例如 pending → success),计数就会实时更新并驱动使用该 Hook 的组件重渲染。
匹配规则:exact / predicate / mutationKey / status 如何生效
真正决定“哪些 mutation 被统计”的是 query-core 中的 matchMutation 函数,位于 packages/query-core/src/utils.ts:182:
export function matchMutation(
filters: MutationFilters,
mutation: Mutation<any, any>,
): boolean {
const { exact, status, predicate, mutationKey } = filters
if (mutationKey) {
if (!mutation.options.mutationKey) {
return false
}
if (exact) {
if (hashKey(mutation.options.mutationKey) !== hashKey(mutationKey)) {
return false
}
} else if (!partialMatchKey(mutation.options.mutationKey, mutationKey)) {
return false
}
}
if (status && mutation.state.status !== status) return false
if (predicate && !predicate(mutation)) return false
return true
}
值得注意的匹配语义:
mutationKey与exact:未设置exact: true时采用partialMatchKey做前缀匹配;设置exact: true时则通过hashKey对 key 做稳定哈希后严格比对,二者哈希结果必须一致。status:要求 mutation 的state.status与给定状态一致。predicate:你可以在谓词中拿到mutation实例,进而访问mutation.options、mutation.state等做任意判断。
实战场景与进阶用法
场景一:应用级“保存中”指示器
当全局有多处表单、多处导出任务同时提交时,可用一个放置在布局层(Layout)的指示器统一表达“仍有变更在路上”:
import { useIsMutating } from '@tanstack/preact-query'
function AppBar() {
const isMutating = useIsMutating()
return (
<header>
{isMutating > 0 && (
<div role="status" aria-live="polite">
{isMutating} 个操作正在保存...
</div>
)}
</header>
)
}
不传 filters 时统计的是整个 MutationCache 中所有 pending mutation,适合希望全局感知的场合。
场景二:按业务域过滤(精确匹配)
若只想针对某一个精确 key 统计,可传入 exact: true:
const isPublishingPost = useIsMutating({
mutationKey: ['posts', 'publish'],
exact: true,
})
场景三:自定义 predicate 过滤
利用 predicate 可结合 mutation 的 meta 或 options 做复杂筛选,例如只统计某个 scope 下的 mutation:
const isMutating = useIsMutating({
predicate: (mutation) => mutation.state.status === 'pending'
&& mutation.options.scope?.id === 'dashboard',
})
场景四:指定自定义 QueryClient
在测试环境或需要隔离 client 的多实例场景中,可通过第二个参数传入独立的 QueryClient,见 packages/preact-query/src/tests/useMutationState.test.tsx:156 中的对应测试:
const queryClient = new QueryClient()
function Page() {
const isMutating = useIsMutating({}, queryClient)
// ...
}
用单元测试验证计数行为
仓库中针对 useIsMutating 的行为有完整的测试覆盖,位于 packages/preact-query/src/tests/useMutationState.test.tsx,可以当作权威的行为规范来阅读:
测试一:计数随 mutation 生命周期变化(见同文件 第 18 行)。测试先后触发两个不同 key、耗时不同的 mutation,断言 isMutatingArray 依次为 [0, 1, 2, 1, 0]——第一个 mutation 启动时计数为 1,第二个启动时变为 2,随两个 mutation 依次完成而逐步降回 1、0。同时测试注释指出,由于内部批处理策略,中间状态也可能出现 [0, 1, 2, 1, 0] 之外如多一次 1 的过渡取值,边界情况下的瞬时取值不被视为稳定契约。
测试二:按 mutationKey 过滤(见 第 81 行)。两个 mutation 分别使用 mutationKey1 与 mutationKey2,但 Hook 只统计 mutationKey1,因此数组断言为 [0, 1, 0]。
测试三:按 predicate 过滤(见 第 117 行)。仅凭 mutation key 前缀无法区分二者时,通过 predicate 比对 mutation.options.mutationKey?.[0] 完成筛选,同样得到 [0, 1, 0]。
测试四:自定义 QueryClient 生效(见 第 156 行)。在不包裹 Provider 的渲染中显式传入 queryClient 后,mutation 触发期间页面文本稳定显示 mutating: 1。
与相关 API 的关系与差异
useIsMutating 是 useMutationState 的面向场景封装。两者的对应关系可归纳为:
| 维度 | useIsMutating |
useMutationState |
|---|---|---|
| 返回值 | 匹配的 pending mutation 数量(number) |
匹配 mutation 的状态数组 |
| 过滤 | filters 上强制叠加 status: 'pending' |
提供 filters 与 select,可选择任意状态与字段 |
| 典型用途 | 全局 / 局部“进行中”指示器 | 读取具体 mutation 的 data、variables 等 |
例如,若你需要展示“哪些 post 正在保存”,应改用 useMutationState 的 select 抽取变量;若你只关心数量,useIsMutating 更简洁。更完整的能力可参考 useMutationState.md、useMutation.md;与查询侧对应的“正在请求的 query 数量”统计则见 useIsFetching.md。
小结
useIsMutating(filters?, queryClient?)返回当前 pending mutation 的数量,实现位于 packages/preact-query/src/useMutationState.ts:35。- 它本质上是
useMutationState的薄封装:在filters上强制注入status: 'pending'后取其结果数组.length。 - 数据来自
QueryClient的MutationCache,通过findAll匹配、subscribe监听,配合replaceEqualDeep与notifyManager实现最小化重渲染。 - 匹配规则由 packages/query-core/src/utils.ts:182 的
matchMutation统一实现:mutationKey默认前缀匹配、exact精确匹配、status与predicate可叠加使用。 - 若项目使用 React、Solid、Vue、Svelte 或 Angular 版本,其对应适配层同样基于相同的
MutationFilters与MutationCache语义,理解本文原理后可平滑迁移。
通过将 useIsMutating 置于布局层并配合 mutationKey 的规范化组织,你可以在几乎零额外开销的前提下,为整个 Preact 应用提供准确、实时的全局提交反馈。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00