TanStack Query(Angular):禁用与暂停查询的两种写法——enabled 选项与 skipToken 深度解析
本篇指南聚焦 TanStack Query Angular 集成(@tanstack/angular-query-experimental)中"如何让一个查询先不发起请求"这一高频实战问题。你将完整掌握两种官方支持的禁用手段——enabled 选项与 skipToken 哨兵值——它们的触发时机、与 Angular Signal 的组合方式、底层在 query-core 中的执行路径,以及二者在行为上的关键差异,从而在"条件请求、参数未就绪、依赖前置数据"等场景下做出正确选择。
问题背景:为什么需要"先禁用"查询
在典型业务中,组件挂载时查询所需的输入往往还没准备好:筛选表单还没提交、userId 还没从登录态拿到、路由参数还是空值。此时如果查询立刻执行,就会发出一次参数错误(fetchTodos(''))或注定失败的请求。TanStack Query 为此提供了两种一等公民式的禁用机制:
enabled: false:在查询选项层面声明"当前不激活",查询会被创建、可以缓存已有数据,但不会主动发起任何请求;queryFn: skipToken:用一个符号值顶替queryFn,从取数函数层面声明"没有可执行的请求"。
二者都适用于 injectQuery(文档示例中的写法;React 对应物为 useQuery),也适用于 injectInfiniteQuery、injectQueries 等全部查询 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: skipToken 与 queryFn: () => T 的分支合并,使得 data 的类型推断在两种模式下依然成立;核心包的类型测试 queryClient.test-d.tsx 专门验证了 "should infer select type with skipToken queryFn" 这一场景。
skipToken 在核心层的执行路径
当某个 fetch 真的走到取数环节而 queryFn === skipToken 时,核心包的 ensureQueryFn(utils.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()
}
从源码结构看,skipToken 与 enabled: 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()))渲染,禁用态下isLoading为false,注意区分"加载中"与"未就绪"。 - 包状态: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 从假翻真即自动发起请求、从真翻假即暂停请求的行为就不再是黑盒,而可以被精确预判与控制。
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 StartedRust0623
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