TanStack Query Angular 实战:injectMutationState 全局 Mutation 状态信号完全解析
在 @tanstack/angular-query-experimental 中,injectMutationState 是一个用于订阅整个应用内所有 mutation 状态的函数式注入 API,它返回一个 Signal,让你无需逐个持有 injectMutation 实例,就能响应式地追踪任意一组变更(例如“所有 pending 状态的登录请求”或“某个 mutationKey 下的全部提交”)。本文基于仓库中的 API 文档与源码实现,完整解析其函数签名、参数含义、类型推导规则与底层响应式机制,读完后你既能正确调用它,也能理解它如何在 NgZone、结构化共享与信号竞态之上保持高效与正确。
API 签名总览
根据 injectMutationState 参考文档,其函数签名为:
function injectMutationState<TResult>(
injectMutationStateFn: () => MutationStateOptions<TResult>,
options?: InjectMutationStateOptions,
): Signal<TResult[]>;
- 功能:注入一个追踪所有 mutation 状态的信号("Injects a signal that tracks the state of all mutations")。
- 默认类型参数:
TResult默认为MutationState<unknown, Error, unknown, unknown>,即返回Signal<MutationState[]>。 - 定义位置:inject-mutation-state.ts(
packages/angular-query-experimental/src/inject-mutation-state.ts第 60 行)。
需要说明的前提:@tanstack/angular-query-experimental 目前仍处于实验阶段,minor 与 patch 版本都可能引入破坏性变更,若在生产环境使用,建议锁定到 patch 级版本号(见 Angular 概览中的 IMPORTANT 声明)。该库兼容 Angular v16 及以上版本。
参数详解
参数一:injectMutationStateFn
文档定义为 () => MutationStateOptions<TResult>,即“一个返回 mutation 状态选项的函数”。在源码中,MutationStateOptions 是 inject-mutation-state.ts 第 23-26 行定义的内部类型:
type MutationStateOptions<TResult = MutationState> = {
filters?: MutationFilters
select?: (mutation: Mutation) => TResult
}
| 选项 | 类型 | 说明 |
|---|---|---|
filters |
MutationFilters(来自 @tanstack/query-core) |
用于筛选要追踪的 mutation,常用字段包括 mutationKey(配合 exact 精确/部分匹配)与 status(如 'pending'、'success'、'error') |
select |
(mutation: Mutation) => TResult |
对每个匹配的 Mutation 实例做投影,把完整的 mutation.state 提取为你真正需要的字段(如 variables、status) |
由于源码中该参数带有默认值 = () => ({})(见 inject-mutation-state.ts 第 61 行),因此你可以完全不带参数调用 injectMutationState(),此时它返回全部 mutation 的 state 数组。
一个值得强调的设计点:injectMutationStateFn 是一个惰性执行的工厂函数,与 injectQuery、injectMutation 采用相同的模式。源码在 computed 内部才会调用 injectMutationStateFn()(见下文原理部分),这意味着你可以在工厂函数内读取其他 Signal(包括组件的 input 输入信号),选项发生变化时结果会自动重算——这一点在 测试用例中有直接验证:
const filterKey = signal(mutationKey1)
const mutationState = TestBed.runInInjectionContext(() => {
return injectMutationState(() => ({
filters: { mutationKey: filterKey(), status: 'pending' },
select: (m) => m.state.variables,
}))
})
expect(mutationState()).toEqual([variables1])
filterKey.set(mutationKey2) // 信号变化
expect(mutationState()).toEqual([variables2]) // 结果随之更新
参数二:options(InjectMutationStateOptions)
第二个参数是 InjectMutationStateOptions 接口,在 inject-mutation-state.ts 第 45-52 行定义,目前只有一个可选属性:
export interface InjectMutationStateOptions {
/**
* The `Injector` in which to create the mutation state signal.
*
* If this is not provided, the current injection context will be used instead (via `inject`).
*/
injector?: Injector
}
| 属性 | 类型 | 说明 |
|---|---|---|
injector |
Injector(可选) |
指定用于创建 mutation state 信号的注入器;不提供时回退到当前注入上下文(通过 inject(Injector)) |
injector 的核心作用是允许在注入上下文之外调用该函数。源码第 64 行的断言逻辑:
!options?.injector && assertInInjectionContext(injectMutationState)
const injector = options?.injector ?? inject(Injector)
即:只有当你显式传入 injector 时,才可以脱离注入上下文调用;否则必须在注入上下文中执行,否则会抛出 NG0203 错误(错误信息会指明 injectMutationState)。注入上下文测试验证了这两种行为:
// 无上下文、无 injector 调用会抛出描述性错误
expect(() => {
injectMutationState()
}).toThrow(/NG0203(.*?)injectMutationState/)
// 显式传入 injector 后,可以在注入上下文之外安全使用
const injector = TestBed.inject(Injector)
expect(injectMutationState(undefined, { injector })).not.toThrow()
返回值与类型推导
返回值是 Signal<TResult[]>——一个追踪所有匹配 mutation 状态的只读信号(数组元素顺序跟随 MutationCache.findAll 的结果)。
TResult 的推导规则由 类型测试文件精确固化:
- 不提供
select:TResult落到默认值MutationState,返回Signal<Array<MutationState>>; - 提供
select:TResult由select的返回类型推导,例如select: (mutation) => mutation.state.status得到Signal<Array<MutationStatus>>。
// 默认:完整 MutationState
const states = injectMutationState(() => ({
filters: { status: 'pending' },
}))
// states(): MutationState[]
// select 推导:只剩 status
const statuses = injectMutationState(() => ({
filters: { status: 'pending' },
select: (mutation) => mutation.state.status,
}))
// statuses(): MutationStatus[]
源码级实现原理
injectMutationState 内部通过四个协作的部分实现“响应式 + 高性能”,完整实现在 inject-mutation-state.ts 第 60-121 行:
1. 依赖解析:从注入器依次取出 DestroyRef、NgZone 与 QueryClient,再通过 queryClient.getMutationCache() 拿到 mutation 缓存——这正是所有 injectMutation 实例共同写入的缓存,因此它能天然地“跨组件”追踪全局变更。
2. 双信号竞态(result from options vs. from subscriber):
const resultFromOptionsSignal = computed(() => {
return [
getResult(mutationCache, injectMutationStateFn()),
performance.now(),
] as const
})
const resultFromSubscriberSignal = signal<[Array<TResult>, number] | null>(null)
resultFromOptionsSignal是一个computed:每次有信号依赖变化(比如工厂函数里读取的input或选项信号)时重新求值,并记录performance.now()时间戳;resultFromSubscriberSignal记录由 MutationCache 订阅回调推送的最新结果及时间戳。
两者的最终取舍由 effectiveResultSignal 决定(第 93-99 行):比较时间戳,谁更新就用谁的结果。从源码结构看,这种设计避免了“选项变化触发 computed 重算”与“缓存订阅回调推送”两条更新路径互相覆盖或产生撕裂状态——总是展示最后生效的那一次计算。
3. 缓存订阅 + Zone 外执行 + 结构化共享(第 101-116 行):
const unsubscribe = ngZone.runOutsideAngular(() =>
mutationCache.subscribe(
notifyManager.batchCalls(() => {
const [lastResult] = effectiveResultSignal()
const nextResult = replaceEqualDeep(
lastResult,
getResult(mutationCache, injectMutationStateFn()),
)
if (lastResult !== nextResult) {
ngZone.run(() => {
resultFromSubscriberSignal.set([nextResult, performance.now()])
})
}
}),
),
)
这里有四层性能保护:
ngZone.runOutsideAngular:订阅注册本身发生在 Zone 之外,mutation 的高频生命周期事件不会白白触发变更检测;notifyManager.batchCalls:同一批次内的多次缓存通知被合并为一次回调;replaceEqualDeep:对旧结果与新结果做深层相等替换(结构化共享),内容没变时直接复用旧数组引用,lastResult !== nextResult判断失败即跳过更新;- 只有确实发生变化时,才通过
ngZone.run回到 Zone 内写入信号,从而触发最小化的变更检测。
4. 生命周期清理:destroyRef.onDestroy(unsubscribe)(第 118 行)确保宿主注入上下文销毁时自动退订 MutationCache,防止内存泄漏。
实战用法:在组件中追踪变更
以下示例基于 测试中的组件用法整理,展示如何用 input.required 作为响应式过滤条件,在模板中实时渲染匹配 mutation 的状态:
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
import {
injectMutation,
injectMutationState,
provideTanStackQuery,
} from '@tanstack/angular-query-experimental'
import { QueryClient } from '@tanstack/query-core'
// 应用根级需提供 QueryClient(provideTanStackQuery 或 provideQueryClient)
// @NgModule: providers: [provideTanStackQuery(new QueryClient())]
@Component({
selector: 'app-task-list',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@for (status of mutationState(); track $index) {
<span>{{ status }}</span>
}
`,
})
export class TaskListComponent {
// 必填输入信号,作为响应式的 mutationKey 过滤条件
category = input.required<string>()
// 追踪该分类下所有 mutation 的状态;
// exact: true 表示精确匹配 ['tasks', category]
mutationState = injectMutationState(() => ({
filters: {
mutationKey: ['tasks', this.category()],
exact: true,
},
select: (m) => m.state.status, // 只取 status,推导为 Signal<MutationStatus[]>
}))
}
运行行为与测试断言一致:两个同 key 的 injectMutation 实例发起变更后,初始渲染为 ['pending', 'pending'];异步完成后自动更新为 ['success', 'error']——全程无需组件手动订阅。
再给出一个典型的“全局提交中”指示器用法(对应 injectIsMutating 文档提到的 app-wide loading 场景):
// 只要存在任意 pending 的保存请求,isSaving 即为 true
isSaving = computed(() =>
injectMutationState(() => ({ filters: { status: 'pending' } }))().length > 0,
)
与 injectIsMutating 的区别在于:injectIsMutating 直接返回 Signal<number>(正在变更的 mutation 数量),而 injectMutationState 提供状态数组,可以结合 filters/select 做任意投影,灵活性更高;injectIsMutating 可以视为它的“计数特化”。
与相关 API 的关系
injectMutation:负责“发起”单个 mutation 变更,其内部状态写入全局MutationCache;injectMutationState负责“旁观”缓存中所有变更,两者互补(见 injectMutation 参考文档)。provideTanStackQuery/provideQueryClient:injectMutationState通过injector.get(QueryClient)工作,因此宿主注入器中必须存在QueryClient提供方,测试中的标准装配方式是providers: [provideTanStackQuery(queryClient)]。- 导出入口:
injectMutationState与InjectMutationStateOptions类型均从包主入口导出,见 index.ts(类型导出于第 39 行export type { InjectMutationStateOptions } from './inject-mutation-state')。
验证依据与延伸阅读
本文所有行为结论均可在当前仓库中复核:
- 实现源码:inject-mutation-state.ts
- 运行时测试:inject-mutation-state.test.ts(覆盖 filters+select 过滤、响应式选项、无参调用、required signal input、NG0203 注入上下文行为)
- 类型推导测试:inject-mutation-state.test-d.ts
- API 文档:injectMutationState.md、InjectMutationStateOptions.md
- 配套示例应用(Angular mutation 实战):examples/angular/optimistic-updates
适用前提提醒:@tanstack/angular-query-experimental 是实验性包,本文描述的 API 细节以当前仓库版本为准,升级前建议对照 CHANGELOG 检查破坏性变更;调用 injectMutationState 时务必处于注入上下文(组件字段初始化、runInInjectionContext 等),否则需显式传入 injector。
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 StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00