Preact Query 指南:禁用与暂停查询(Disabling / Pausing Queries)
【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query
在基于 Preact 的前端应用中,@tanstack/preact-query的useQuery并不总是希望组件挂载后立刻发起请求——例如"用户点击后再加载"、"过滤条件尚未填写时不请求"、或"依赖项还不存在时等待"。本指南围绕enabled选项与skipToken两大机制,系统讲解如何禁用、延迟、按条件启动查询,并厘清isPending、isFetching、isLoading在禁用场景下的区别。读完本文,你将能正确实现"惰性查询 / 依赖查询",并在保持 TypeScript 类型安全的前提下禁用查询。
本文对应仓库文档为 Preact Query 禁用查询指南,该文档与 React Query 同主题指南 内容一一对应(前者由后者以react-query → preact-query、React → Preact全局替换生成),文中引用的实现代码均来自当前仓库。
一、用enabled选项禁用自动执行
想要让查询"不自动运行",最直接的入口是useQuery选项中的enabled。将其置为false,查询便进入禁用状态;queryObserver.ts 中对enabled做了校验,它既可以是一个布尔值,也可以是一个返回布尔值的回调函数——当回调形式出现时,会在每次结果计算前通过resolveQueryValue取值,这意味着你可以在渲染期间基于最新 props / state 动态决定是否启用查询。
import { useQuery } from '@tanstack/preact-query' function Todos() { const { isLoading, isError, data, error, refetch, isFetching } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, enabled: false, }) return ( <div> <button onClick={() => refetch()}>Fetch Todos</button> {data ? ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) : isError ? ( <span>Error: {error.message}</span> ) : isLoading ? ( <span>Loading...</span> ) : ( <span>Not ready ...</span> )} <div>{isFetching ? 'Fetching...' : null}</div> </div> ) }enabled = false时查询的五种行为
当enabled为false时,查询呈现以下特征:
- 已有缓存数据:若该
queryKey此前已经取得过数据,查询会被初始化为status === 'success'(即isSuccess)状态,界面可立即渲染缓存内容; - 没有缓存数据:查询从
status === 'pending'、fetchStatus === 'idle'开始——即"还没有数据,但也并没有在请求"; - 挂载时不自动请求:不会在组件挂载时触发
fetch; - 不参与后台自动刷新:不会响应窗口聚焦(window focus)、网络重连等触发条件进行后台 refetch;
- 忽略失效 / 刷新指令:
queryClient.invalidateQueries与queryClient.refetchQueries对它的常规调用会被忽略。
在底层实现中,这一系列约束正是由enabled贯穿query-core的多处判断完成的:query.ts 中查询的isActive()只统计enabled !== false的观察者,isStale()等失效判断同样把enabled === false的查询排除在外;queryObserver.ts 中shouldFetchOnMount、shouldFetchOn、shouldFetchOptionally均以resolveQueryValue(options.enabled, query) !== false作为前置门槛,从源头杜绝了"挂载自动请求"与"后台按需刷新"的触发。
与此同时,useQuery返回的refetch依旧可用:上述示例中点击按钮调用refetch()会手动触发一次请求。这是禁用态下唯一常规的拉取方式。
二、永久禁用并非最佳实践:声明式优于命令式
文档特别提醒:永久禁用(enabled: false永远不变)会让查询退出 TanStack Query 带来的大量特性(例如后台刷新、自动失效),同时也偏离了该库惯用的声明式用法——你实际上是在"依赖声明"与"命令式手动拉取"之间切换到了后者;而且refetch无法携带参数(你无法通过refetch(someArgs)把运行时入参传给queryFn)。
大多数情况下,你真正需要的其实是一个惰性查询(lazy query):查询先不请求,等到某个前置条件满足后再"自然地"启动——也就是把enabled从false变为true。
三、惰性查询:条件满足时自动启动
enabled的价值不在于"永久关死",而在于在稍后某个时机由 false 翻转为 true。最经典的例子是过滤器表单:只有用户输入了过滤值,才发起第一次请求。
下方代码展示的是仓库 Preact 禁用查询指南 中的示例。filter为空字符串时enabled: !!filter为false,查询处于禁用态;一旦用户提交过滤条件,setFilter更新状态,enabled变为true,查询自动执行:
function Todos() { const [filter, setFilter] = useState('') const { data } = useQuery({ queryKey: ['todos', filter], queryFn: () => fetchTodos(filter), // ⬇️ disabled as long as the filter is empty enabled: !!filter, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) }注意queryKey与queryFn都依赖filter——把filter放进queryKey后,每次过滤条件变化都会形成新的缓存条目,这是"依赖查询"的标准写法。更多场景可延伸阅读 依赖查询(Dependent Queries) 与 查询基础(Queries)。
isLoading与isPending/isFetching的区别
惰性查询启动时status === 'pending'自始至终成立——因为pending的语义是"还没有数据",这在禁用期间也是成立的。但它并不能用来驱动 loading 动画:禁用态下数据确实不存在(isPending为 true),可此时根本没有在请求。
正确的选择是使用派生标记isLoading(旧版称isInitialLoading)。在 queryObserver.ts 中可以看到它的确切定义:
const isFetching = newState.fetchStatus === 'fetching' const isPending = status === 'pending' const isLoading = isPending && isFetching // 只在“首次请求进行中”为 true即isLoading = isPending && isFetching:只有当查询正在第一次拉取数据时才为true。因此:
- 禁用期(
enabled: false、无数据)→isPending: true、isFetching: false、isLoading: false,适合展示"Not ready..."之类的占位; - 首次请求进行中 →
isPending: true、isFetching: true、isLoading: true,可展示 spinner; - 请求完成 → 进入
success,不再适用 loading 标记。
在第一节的完整示例中,正是通过isLoading区分"Loading..."与"Not ready ..."两种 UI 状态。
四、TypeScript 类型安全禁用:skipToken
如果你在使用 TypeScript,并且希望基于某个条件禁用查询,同时保持完整的类型推导,那么enabled = false有一个更优的替代方案——skipToken:
import { skipToken, useQuery } from '@tanstack/preact-query' function Todos() { const [filter, setFilter] = useState<string | undefined>() const { data } = useQuery({ queryKey: ['todos', filter], // ⬇️ disabled as long as the filter is undefined or empty queryFn: filter ? () => fetchTodos(filter) : skipToken, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) }filter为undefined时queryFn传入skipToken,查询被禁用;一旦filter有值,queryFn立即替换为真正的取数函数。相比enabled,它的优势是类型层面的安全性:在filter可能为undefined的场景下,你无需编写fetchTodos(filter!)这种非空断言——类型系统会保证queryFn只在filter存在时才可能被真正调用。
实现层面:skipToken是什么?
skipToken定义于 query-core/src/utils.ts,本质上是一个Symbol(),并导出了对应的类型别名SkipToken;- 在
QueryClient的默认值归一化阶段,queryClient.ts 会做显式转换:
if (defaultedOptions.queryFn === skipToken) { defaultedOptions.enabled = false }也就是说,skipToken在运行时会被自动映射为enabled: false,两者行为等价(唯一的差别见下文"refetch 限制")。正因如此,@tanstack/preact-query完整重导出了query-core(见 packages/preact-query/src/index.ts),你才能直接import { skipToken } from '@tanstack/preact-query',无需单独引入 query-core。
refetch与skipToken的限制(重要)
重要:当
queryFn为skipToken时,useQuery返回的refetch()不会生效。此时调用refetch()会抛出Missing queryFn错误——因为没有可执行的查询函数。若你需要手动触发查询,请改用enabled: false,它对refetch()是放行的。除此之外,skipToken与enabled: false表现完全一致。
原因同样在源码中:当queryFn被解析时,utils.ts 中的 ensureQueryFn 会对queryFn === skipToken(或缺失)的情况返回一个"必定 reject"的函数,错误信息即Missing queryFn: '<queryHash>'。因此手动refetch拿到的是一个必然失败的查询函数。开发环境下若误调,utils.ts 还会额外在控制台打印一条配置错误提示。
skipToken的适用边界
需要留意skipToken并非任何查询 API 都能使用:
useQuery/useQueries/useInfiniteQuery:支持,可通过queryOptions()/infiniteQueryOptions()与它们搭配(见 queryOptions.ts 中skipToken重载的 JSDoc 示例);- Suspense 系列(
useSuspenseQuery、useSuspenseInfiniteQuery、useSuspenseQueries):不允许传入skipToken。Suspense 模式的 Hook 无法渲染"禁用态",因此 useSuspenseQuery.ts 等实现会在开发环境打印skipToken is not allowed for useSuspenseQuery之类的错误,类型层面也通过重载将其排除(参见 types.ts 相关注释); - Prefetch 系列(
usePrefetchQuery、usePrefetchInfiniteQuery):同样不允许,因为预取必然需要一个真正执行的查询函数(见 types.ts)。
五、禁用状态建模小结
| 目标 | 推荐方式 | refetch()手动触发 | 类型安全 |
|---|---|---|---|
| 永久禁用 + 手动按钮触发 | enabled: false | ✅ 可用 | 需要自己处理可空入参 |
| 条件满足后自动启动(惰性查询) | enabled: !!condition或回调 | — | 需要自己处理可空入参 |
| 条件满足后自动启动 + 全程类型安全 | queryFn: cond ? fn : skipToken | ❌ 不可用(Missing queryFn) | ✅ 编译器保证 |
| Suspense / Prefetch 下的禁用 | 用enabled: false,不要用skipToken | — | 遵循对应 Hook 的类型约束 |
辅助判断 UI 状态时,请记住:isLoading = isPending && isFetching,只有它才能表达"正在第一次加载";禁用但无数据时用isPending单独判断,展示 "Not ready" 类占位即可。
六、结语
禁用与暂停查询是 TanStack Query 数据流控制的重要一环。enabled负责"声明式地描述查询的运行依赖",适合实现过滤器、搜索框等典型的惰性查询;skipToken则在 TypeScript 场景下把同一思路做到了类型闭环。理解isPending/isFetching/isLoading的区分,以及refetch在skipToken下的限制,是避免"按钮点了没反应""loading 永远转圈"等经典踩坑的关键。建议结合实际源码与 React Query 对应版本 对照阅读,两个框架的指南内容与行为完全一致。
【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考