@tanstack/preact-query 无限查询预取的类型契约:UsePrefetchInfiniteQueryOptions深度解析
【免费下载链接】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
UsePrefetchInfiniteQueryOptions是 TanStack Query 的 Preact 适配层(@tanstack/preact-query)中为usePrefetchInfiniteQuery定义的选项类型,它刻画了"在渲染期预取无限查询(infinite query)"时允许传入的全部配置。读完本文,你将掌握该类型的完整声明结构、每个类型参数的职责与默认值、queryFn为何禁止skipToken,以及它在渲染期预取与 Suspense 协作背后的运行时语义,并能在自己的 Preact 组件里正确、类型安全地写出"fetch-as-you-render"式的分页预取代码。
该类型在 preact-query 中的定位
在 packages/preact-query/src/types.ts 中,所有 hook 的选项类型都遵循一条设计主线:UI 层类型 = 复用@tanstack/query-core的命令式执行选项 + 剔除 / 收紧不适合 hook 场景的字段。UsePrefetchInfiniteQueryOptions正是为usePrefetchInfiniteQuery提供参数的类型:
- 普通查询的预取对应 UsePrefetchQueryOptions;
- 无限查询的预取则对应本主题
UsePrefetchInfiniteQueryOptions(packages/preact-query/src/types.ts#L117)。
两者与 Suspense 类 hook(如UseSuspenseInfiniteQueryOptions)形成"预取 → 渲染"的成对关系,共同支撑 Suspense 指南 所描述的"渲染期预取 + 边界内消费"模式。
完整的类型声明与继承关系
原文给出的核心声明如下(定义于 packages/preact-query/src/types.ts:117):
type UsePrefetchInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> = DistributiveOmit<InfiniteQueryExecuteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>, "queryFn"> & object;它表达的语义是:usePrefetchInfiniteQuery接受的选项,与queryClient.infiniteQuery完全一致——也就是说,除了queryFn在未定义默认查询函数时必须显式提供之外,其余所有字段都与命令式执行选项相同。
结合底层源码可以拆解出整条继承链:
顶层使用
DistributiveOmit<..., "queryFn">剔除queryFn(联合分发式剔除,其定义见 packages/query-core/src/types.ts#L14:TObject extends any ? Omit<TObject, TKey> : never)。被剔除的基座
InfiniteQueryExecuteOptions定义于 packages/query-core/src/types.ts#L572,其结构为:Omit<QueryExecuteOptions<..., InfiniteData<TQueryFnData, TPageParam>, ...>, 'initialPageParam'> & InitialPageParam<TPageParam> & InfiniteQueryPages<TQueryFnData, TPageParam>QueryExecuteOptions(见 packages/query-core/src/types.ts#L492)在QueryOptions之上强制queryKey必填,并提供select与staleTime。无限查询通过InitialPageParam与InfiniteQueryPages两个交叉类型叠加了分页语义:initialPageParam必填,作为首屏页码;pages+getNextPageParam是一组可选的批量抓取方式(多页一次性预取时使用),也可省略只抓首页。
于是最终落到UsePrefetchInfiniteQueryOptions上的实际约束是:queryKey、initialPageParam、getNextPageParam(在未指定pages的逐页模式下的后续翻页依据)都始终要求提供——这一点在 hook 的 JSDoc(packages/preact-query/src/usePrefetchInfiniteQuery.tsx#L10)中有明确说明,也由类型测试用例"should require initialPageParam and getNextPageParam"(packages/preact-query/src/tests/usePrefetchInfiniteQuery.test-d.tsx)做了编译期验证。
关键成员:queryFn的特殊收紧
optional queryFn: Exclude<InfiniteQueryExecuteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>["queryFn"], SkipToken>;queryFn是唯一被类型系统主动改造的字段,规则有两层:
skipToken被排除在合法值之外。SkipToken是 query-core 导出的特殊占位符,用于表达"当前尚无可用参数,请暂停查询"(例如在useQuery中用enabled类似的效果实现条件查询);但预取场景里,一次预取"必须真的发起一次请求"才能给后续 Suspense 消费方铺好数据,因此在类型层面直接将其剔除。类型测试用例"should not allow skipToken in queryFn"(usePrefetchInfiniteQuery.test-d.tsx)同时验证了直接传skipToken与三元表达式下出现skipToken两种写法都会报类型错误。- 唯一的例外是定义了默认查询函数。如果你通过
new QueryClient({ defaultOptions: { queries: { queryFn } } })配置了全局默认查询函数,那么queryFn可以整体省略——预取会回落到默认实现。这也正是类型声明中该字段为optional(可选)而非必填的原因。
与之相对的,"观察者(observer)式" hook 里常出现的enabled、refetchInterval、throwOnError这类与订阅、重渲染相关的选项,在queryClient命令式执行的选项体系中本身就不存在(预取是单次点火,不是常驻观察),所以也无法传给预取 hook。类型测试"should not allow refetchInterval, enabled or throwOnError options"(usePrefetchInfiniteQuery.test-d.tsx)逐一对这三个字段做了@ts-expect-error校验,说明这些字段在编译期就会被拒绝。
五个类型参数逐一解读
该类型把"单页数据类型、错误类型、最终数据类型、缓存键类型、翻页参数类型"拆成了 5 个独立维度,方便类型推断与复用:
| 类型参数 | 约束 / 默认值 | 含义 |
|---|---|---|
TQueryFnData | = unknown | 你的queryFn解析出的单个页面的数据类型。注意它是一页(如Project[]或{ items, nextCursor }),而非InfiniteData<...>包装后的整体。 |
TError | = DefaultError | 你的queryFn可能抛出的错误类型,默认是 query-core 的DefaultError。 |
TData | = TQueryFnData | select执行后data最终呈现的类型。此处默认值刻意设为"单个页面",原因是预取本身永远不会把data读回给你(hook 返回void),这个参数只在"你把这组 options 复用到其他带select的地方"时才真正影响类型结果。 |
TQueryKey | extendsQueryKey,默认QueryKey | 你的queryKey的类型;泛型约束保证了它必须是 query-core 认可的缓存键形态(数组)。 |
TPageParam | = unknown | 传给queryFn用于抓取某一页的参数类型,即queryFn上下文里pageParam的类型。 |
将它与 UseInfiniteQueryOptions(普通useInfiniteQuery的选项类型)对比,能看出一个刻意为之的差异:后者TData默认是InfiniteData<TQueryFnData>(所有已抓取页面+页码的整体形状),因为 hook 会把完整数据交还给你渲染;而预取版本的TData默认退回单个页面,因为UsePrefetchInfiniteQueryOptions从设计上就不需要描述"读出来是什么",只需保证传给queryClient.infiniteQuery时能被正确推断。
运行时行为:这个类型驱动的 hook 到底做了什么
理解了选项类型,再看它的消费者实现会更清晰。usePrefetchInfiniteQuery 的完整逻辑非常短:
export function usePrefetchInfiniteQuery<TQueryFnData, TError, TData, TQueryKey extends QueryKey, TPageParam>( options: UsePrefetchInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>, queryClient?: QueryClient, ) { const client = useQueryClient(queryClient) if (!client.getQueryState(options.queryKey)) { void client.infiniteQuery(options).catch(noop) } }几个值得注意的运行时事实:
- 渲染期点火:hook 不返回任何值(
void),纯粹为了在组件渲染、且位于某个包裹 Suspense 消费方的边界之前发起预取,从而让useSuspenseInfiniteQuery挂起时数据已经在途或就绪。 - 缓存守卫:只有当
client.getQueryState(options.queryKey)为空——即该缓存键从未存在任何状态时才发起预取;如果已存在pending/error等状态,则直接跳过。因此"每次渲染都调用"非常廉价,不会重复抓取已有数据或已在途的数据。 - 静默吞错:通过
.catch(noop)把预取失败吞掉,避免在渲染阶段产生未处理的 Promise 拒绝。 - 底层执行:
queryClient.infiniteQuery(见 packages/query-core/src/queryClient.ts#L437)会设置options._type = 'infinite'后走统一的query执行管线,返回InfiniteData<...>或select之后的TData。
运行时测试 usePrefetchInfiniteQuery.test.tsx 验证了这一整套流程:在Page内部通过useSuspenseInfiniteQuery(queryOpts)消费同一个 query key 时,预取在父组件渲染期便已把第一页数据写入缓存,Suspense 挂起后随即以已抓取数据恢复渲染。
实战示例:类型参数如何被推断
把上面的类型知识组合成一个典型用例。下面这组代码取自 hook JSDoc 的示例模式(见 usePrefetchInfiniteQuery.tsx#L27 附近),并加入了显式类型标注以展示 5 个参数如何落位:
import { Suspense } from 'preact/compat' import { infiniteQueryOptions, usePrefetchInfiniteQuery, } from '@tanstack/preact-query' interface ProjectPage { items: string[] nextId: number | undefined } const projectsOptions = infiniteQueryOptions({ queryKey: ['projects'], // pageParam 的类型由 initialPageParam / getNextPageParam 推断为 number queryFn: ({ pageParam }) => fetchProjects(pageParam), initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextId, }) function App() { // 在 Suspense 边界之前触发预取,返回 void usePrefetchInfiniteQuery(projectsOptions) return ( <Suspense fallback={<h1>Loading projects...</h1>}> <Projects /> </Suspense> ) }这里TQueryFnData = ProjectPage、TPageParam = number(由initialPageParam: 0推断),TQueryKey = ['projects']对应的字面量数组类型,TError、TData保持默认。若误把queryFn写成skipToken,或试图传入enabled、refetchInterval,TypeScript 都会立即给出编译错误——这正是本类型提供的"约束即文档"价值。
借助infiniteQueryOptions(它会根据queryFn与initialPageParam反推出精确的TQueryFnData、TPageParam),options 还能被同时复用到useSuspenseInfiniteQuery与预取 hook,保证消费方拿到的data.pages形状与预取写入缓存的形状始终一致。
使用注意与常见误区
- 不要期待返回值:预取选项里
TData默认是"单页"而非InfiniteData,正是因为结果不会回读。如果你在别处复用 options 时才需要关心select输出。 skipToken在这里无效:预取必须在渲染期真正发一次请求;若请求参数尚不可知,应当延迟渲染这个发起预取的组件本身,而不是靠skipToken挂起。- 缓存命中即跳过:利用这一守卫,可以放心地在每次渲染调用,而无需手动判断
isFetching之类状态。 - 第二参数
queryClient:类型签名允许传入自定义QueryClient,否则从最近的 Provider 上下文取用,适合在测试或隔离场景中注入独立实例。
相关源码路径速查
- 类型定义本体:packages/preact-query/src/types.ts#L117
- 基座类型
InfiniteQueryExecuteOptions与DistributiveOmit:packages/query-core/src/types.ts#L572、packages/query-core/src/types.ts#L14 - 消费该类型的 hook 实现:packages/preact-query/src/usePrefetchInfiniteQuery.tsx
- 底层命令式执行
queryClient.infiniteQuery:packages/query-core/src/queryClient.ts#L437 - 类型级测试(参数约束、
skipToken拒绝、无返回值):packages/preact-query/src/tests/usePrefetchInfiniteQuery.test-d.tsx - 运行时测试(渲染期预取 + Suspense 消费):packages/preact-query/src/tests/usePrefetchInfiniteQuery.test.tsx
- 同文件的成对类型(普通查询预取、Suspense 无限查询选项):packages/preact-query/src/types.ts#L79、packages/preact-query/src/types.ts#L279
【免费下载链接】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),仅供参考