首页
/ TanStack Query(Angular):禁用与暂停查询的两种写法——enabled 选项与 skipToken 深度解析

TanStack Query(Angular):禁用与暂停查询的两种写法——enabled 选项与 skipToken 深度解析

2026-09-05 17:11:42作者:裘晴惠Vivianne

本篇指南聚焦 TanStack Query Angular 集成(@tanstack/angular-query-experimental)中"如何让一个查询先不发起请求"这一高频实战问题。你将完整掌握两种官方支持的禁用手段——enabled 选项与 skipToken 哨兵值——它们的触发时机、与 Angular Signal 的组合方式、底层在 query-core 中的执行路径,以及二者在行为上的关键差异,从而在"条件请求、参数未就绪、依赖前置数据"等场景下做出正确选择。

问题背景:为什么需要"先禁用"查询

在典型业务中,组件挂载时查询所需的输入往往还没准备好:筛选表单还没提交、userId 还没从登录态拿到、路由参数还是空值。此时如果查询立刻执行,就会发出一次参数错误(fetchTodos(''))或注定失败的请求。TanStack Query 为此提供了两种一等公民式的禁用机制:

  1. enabled: false:在查询选项层面声明"当前不激活",查询会被创建、可以缓存已有数据,但不会主动发起任何请求;
  2. queryFn: skipToken:用一个符号值顶替 queryFn,从取数函数层面声明"没有可执行的请求"。

二者都适用于 injectQuery(文档示例中的写法;React 对应物为 useQuery),也适用于 injectInfiniteQueryinjectQueries 等全部查询 API。

方式一:静态禁用——enabled: false

最直接的场景:查询定义在组件里,但要等用户点击按钮才拉取数据。官方文档示例如下(完整继承自 disabling-queries.md):

@Component({
  selector: 'todos',
  template: `<div>
    <button (click)="query.refetch()">Fetch Todos</button>

    @if (query.data()) {
      <ul>
        @for (todo of query.data(); track todo.id) {
          <li>{{ todo.title }}</li>
        }
      </ul>
    } @else {
      @if (query.isError()) {
        <span>Error: {{ query.error().message }}</span>
      } @else if (query.isLoading()) {
        <span>Loading...</span>
      } @else if (!query.isLoading() && !query.isError()) {
        <span>Not ready ...</span>
      }
    }

    <div>{{ query.isLoading() ? 'Fetching...' : '' }}</div>
  </div>`,
})
export class TodosComponent {
  query = injectQuery(() => ({
    queryKey: ['todos'],
    queryFn: fetchTodoList,
    enabled: false, // 静态禁用:挂载时不发请求
  }))
}

要点:

  • 设置 enabled: false 后,组件挂载、QueryClient 缓存恢复等事件都不会触发 fetchTodoList 执行;
  • query.refetch() 仍可手动调用——文档示例中"Fetch Todos"按钮正是走这条手动通道。手动 refetch 会强制发起请求,即使查询处于禁用状态;
  • 禁用期间模板会落入 Not ready ... 分支:没有数据、没有错误、也没有加载中状态。

为什么 Angular 中 refetch 前要先同步最新选项

从源码看,injectQuery 返回的结果对象对 refetch 做了一层包装(见 create-base-query.ts):

const originalRefetch = result.refetch
return {
  ...result,
  refetch: ((...args: Parameters<typeof originalRefetch>) => {
    observer.setOptions(defaultedOptionsSignal()) // 先把最新 options 同步给 observer
    return originalRefetch(...args)
  }) as typeof originalRefetch,
}

这解释了上一节示例中按钮 (click)="query.refetch()" 为何能拿到最新的 enabled/queryKey 语义——每次点击前都会把当前计算出的 options 重新灌入 observer。

方式二:响应式禁用——enabled 与 Signal 组合

Angular 集成的核心特色:传给 injectQuery 的选项函数运行在响应式上下文中(源码注释将其类比为 computed)。选项里任何被读到的 Signal,都会在变更时自动重算选项、重新订阅查询。这就是文档中第二个示例的完整代码:

@Component({
  selector: 'todos',
  template: `
    <div>
      // 🚀 应用筛选条件会启用查询并立即执行
      <filters-form onApply="filter.set" />
      <todos-table data="query.data()" />
    </div>
  `,
})
export class TodosComponent {
  filter = signal('')

  todosQuery = injectQuery(() => ({
    queryKey: ['todos', this.filter()],
    queryFn: () => fetchTodos(this.filter()),
    enabled: !!this.filter(), // 筛选值非空才启用
  }))
}

这段代码的完整生命周期是:

阶段 filter 行为
初始 ''(falsy) 查询被禁用,不发请求;queryKey['todos', '']
用户提交筛选 'active' 等 truthy 值 enabled 翻转为 true,同时 queryKey 变更,查询自动启用并立即执行
用户清空筛选 '' 查询回到禁用状态,不再自动请求

inject-query.ts 的 JSDoc 可以看到官方对这一行为的明确描述:"the query will be automatically enabled and executed when the filter signal changes to a truthy value. When the filter signal changes back to a falsy value, the query will be disabled."(当 filter 信号变为真值时查询自动启用并执行;变回假值时被禁用。)

响应式链路在源码中的位置

响应式能力来自 create-base-query.ts 的两个关键构造:

// 1) options 被包进 computed(),因此读取到的 signal 全部进入依赖追踪
const defaultedOptionsSignal = computed(() => {
  const defaultedOptions = queryClient.defaultQueryOptions(optionsFn())
  defaultedOptions._optimisticResults = isRestoring() ? 'isRestoring' : 'optimistic'
  return defaultedOptions
})

// 2) effect 在依赖变化时把新 options 推给 QueryObserver
effect((onCleanup) => {
  const observer = observerSignal()
  const defaultedOptions = defaultedOptionsSignal()
  untracked(() => {
    observer.setOptions(defaultedOptions)
  })
  onCleanup(() => {
    ngZone.run(() => resultFromSubscriberSignal.set(null))
  })
}, { allowSignalWrites: VERSION.major < '19' || undefined })

也就是说,enabled 并非"挂载时取一次值"的静态字段:filter.set('active') 会触发 defaultedOptionsSignal 重算 → effect 重跑 → observer.setOptions → observer 检测到 enabled 由假变真,走 executeFetch 发出请求。整个链路无需任何手动 refetch

方式三:queryFn: skipToken——从取数函数层面禁用

文档给出的第三个示例换了一种禁用思路:不传 enabled,而是在条件不满足时直接把 queryFn 设为 skipToken

import { skipToken, injectQuery } from '@tanstack/angular-query-experimental'

@Component({
  selector: 'todos',
  template: `
    <div>
      // 🚀 应用筛选条件会启用查询并立即执行
      <filters-form onApply="filter.set" />
      <todos-table data="query.data()" />
    </div>
  `,
})
export class TodosComponent {
  filter = signal('')

  todosQuery = injectQuery(() => ({
    queryKey: ['todos', this.filter()],
    queryFn: this.filter() ? () => fetchTodos(this.filter()) : skipToken,
  }))
}

skipToken 在核心包中定义为一个 Symbol,并配套 SkipToken 类型(见 utils.ts):

export const skipToken = Symbol()
export type SkipToken = typeof skipToken

类型系统会把 queryFn: skipTokenqueryFn: () => T 的分支合并,使得 data 的类型推断在两种模式下依然成立;核心包的类型测试 queryClient.test-d.tsx 专门验证了 "should infer select type with skipToken queryFn" 这一场景。

skipToken 在核心层的执行路径

当某个 fetch 真的走到取数环节而 queryFn === skipToken 时,核心包的 ensureQueryFnutils.ts)会:

  • 开发环境(NODE_ENV !== 'production')输出控制台错误,提示这是一个配置错误;
  • 返回一个永远 reject 的函数(Missing queryFn: '<queryHash>'),使本次 fetch 失败。

而在 Query 的状态判定上(见 query.ts):

isActive(): boolean {
  return this.observers.some(
    (observer) => resolveQueryValue(observer.options.enabled, this) !== false,
  )
}

isDisabled(): boolean {
  if (this.getObserversCount() > 0) {
    return !this.isActive()
  }
  // 无 observer 时,queryFn 为 skipToken 或从未发起过 fetch 的查询都视为 disabled
  return this.options.queryFn === skipToken || !this.isFetched()
}

从源码结构看,skipTokenenabled: false 最终殊途同归:两者都让查询停留在 idle 的 fetch 状态、不自动发请求。差异集中在边界行为上。

两种方式的差异对比与选型

维度 enabled: false queryFn: skipToken
声明位置 查询选项 enabled 字段 取数函数本身
与 Signal 响应式组合 直接写 enabled: !!this.filter() 写三元表达式切换 queryFn
是否阻止自动 fetch 是(observer 判定不激活) 是(fetch 时取数函数被拒绝/跳过)
手动 refetch() 可以强制发起请求 仍无真实 queryFn 可执行,请求无法成功
语义表达 "这个查询当前不激活" "当前根本没有请求可发"
典型场景 按钮触发、暂不拉取但保留缓存 参数缺失导致请求本身无法构造

核心包针对 skipToken 的单元测试(queryClient.test.tsx)刻画了它在命令式 client.query() 场景下的精确语义:无缓存数据时 reject,有缓存数据时直接返回缓存("should return cached data when skipToken is provided")。在本文讨论的 injectQuery 声明式场景下,两者的日常表现非常接近,可按可读性任选。

从仓库源码的注释倾向看(injectQuery 的 JSDoc 与 ensureQueryFn 的报错文案均把 skipToken 触发 fetch 描述为"配置错误"),文档主推 enabled 作为条件控制开关、skipToken 作为取数函数缺失时的兜底表达,这一顺序与官方 disabling-queries 文档的示例编排一致。

实用细节与注意事项

  • 禁用不影响缓存:查询被禁用后,已有数据仍然保留在 QueryClient 缓存中,query.data() 继续可读;重新启用时若数据在 staleTime 内,可能先展示缓存、视情况决定是否后台刷新。
  • 禁用期间的全局失效不会自动请求该查询enabled 为假的 observer 不属于"active observer"(isActive 的实现可佐证),queryClient.invalidateQueries 等失效操作对其的自动 refetch 语义受激活状态约束。
  • queryKey 要包含条件变量:响应式示例中 queryKey: ['todos', this.filter()] 把筛选值放进 key,保证不同筛选条件各自独立缓存;只改 enabled 而 key 不变时,重新启用会复用同一份缓存。
  • 类型收窄enabled: false 时模板侧仍建议配合 query.data() 的真值判断(@if (query.data()))渲染,禁用态下 isLoadingfalse,注意区分"加载中"与"未就绪"。
  • 包状态:Angular 集成位于 packages/angular-query-experimental(包名 @tanstack/angular-query-experimental),属于 experimental 渠道,API 以该包内源码为准;安装方式见 installation.md
  • 默认查询级配置injectQuery 内部的 queryClient.defaultQueryOptions(...)(见 create-base-query.ts)意味着你可以在 QueryClient 层设置全局默认 enabled 等选项,组件级选项会覆盖它们。

小结

在 TanStack Query 的 Angular 集成中,"禁用查询"有两把标准工具:enabled 是查询选项级的激活开关,天然融入 Angular Signal 的响应式重算,适合"条件满足才请求"的主流场景;skipToken 是取数函数级的哨兵,适合"请求本身尚不存在"的场景,并与类型系统无缝协作。理解了 create-base-query.ts 中 computed + effect 把 options 推送给 QueryObserver 的链路后,enabled 从假翻真即自动发起请求、从真翻假即暂停请求的行为就不再是黑盒,而可以被精确预判与控制。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.79 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
988
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384