首页
/ TanStack Query Angular 实战:injectMutationState 全局 Mutation 状态信号完全解析

TanStack Query Angular 实战:injectMutationState 全局 Mutation 状态信号完全解析

2026-09-06 18:08:55作者:戚魁泉Nursing

@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.tspackages/angular-query-experimental/src/inject-mutation-state.ts 第 60 行)。

需要说明的前提:@tanstack/angular-query-experimental 目前仍处于实验阶段,minor 与 patch 版本都可能引入破坏性变更,若在生产环境使用,建议锁定到 patch 级版本号(见 Angular 概览中的 IMPORTANT 声明)。该库兼容 Angular v16 及以上版本。

参数详解

参数一:injectMutationStateFn

文档定义为 () => MutationStateOptions<TResult>,即“一个返回 mutation 状态选项的函数”。在源码中,MutationStateOptionsinject-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 提取为你真正需要的字段(如 variablesstatus

由于源码中该参数带有默认值 = () => ({})(见 inject-mutation-state.ts 第 61 行),因此你可以完全不带参数调用 injectMutationState(),此时它返回全部 mutationstate 数组。

一个值得强调的设计点:injectMutationStateFn 是一个惰性执行的工厂函数,与 injectQueryinjectMutation 采用相同的模式。源码在 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 的推导规则由 类型测试文件精确固化:

  1. 不提供 selectTResult 落到默认值 MutationState,返回 Signal<Array<MutationState>>
  2. 提供 selectTResultselect 的返回类型推导,例如 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. 依赖解析:从注入器依次取出 DestroyRefNgZoneQueryClient,再通过 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 变更,其内部状态写入全局 MutationCacheinjectMutationState 负责“旁观”缓存中所有变更,两者互补(见 injectMutation 参考文档)。
  • provideTanStackQuery / provideQueryClientinjectMutationState 通过 injector.get(QueryClient) 工作,因此宿主注入器中必须存在 QueryClient 提供方,测试中的标准装配方式是 providers: [provideTanStackQuery(queryClient)]
  • 导出入口injectMutationStateInjectMutationStateOptions 类型均从包主入口导出,见 index.ts(类型导出于第 39 行 export type { InjectMutationStateOptions } from './inject-mutation-state')。

验证依据与延伸阅读

本文所有行为结论均可在当前仓库中复核:

适用前提提醒@tanstack/angular-query-experimental 是实验性包,本文描述的 API 细节以当前仓库版本为准,升级前建议对照 CHANGELOG 检查破坏性变更;调用 injectMutationState 时务必处于注入上下文(组件字段初始化、runInInjectionContext 等),否则需显式传入 injector

登录后查看全文
热门项目推荐
相关项目推荐