Preact Query 的 infiniteQueryOptions以强类型配置工厂串联 useInfiniteQuery 与命令式 API【免费下载链接】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导读在 TanStack Query本仓库中的 Preact Query 实现包名tanstack/preact-query中无限滚动/分页加载通常由useInfiniteQuery完成而infiniteQueryOptions则是一个类型安全的配置工厂函数把「一份可用于无限查询的 options 对象」从组件内部抽离出来使其能在useInfiniteQuery、useSuspenseInfiniteQuery、预取辅助以及queryClient.infiniteQuery等命令式 API 之间自由复用。读完本文你将掌握infiniteQueryOptions的三个函数重载各自适用于什么场景、initialData与skipToken如何影响返回的数据类型以及如何用参数化工厂模式为不同业务键复用同一份查询配置并理解其背后的类型标记DataTag与分页参数流转原理。infiniteQueryOptions是什么为什么要使用它infiniteQueryOptions的定位非常清晰凡是能传给useInfiniteQuery的选项都可以传给infiniteQueryOptions。它的输入输出在运行时几乎不做任何加工源码中最终实现只是把 options 原样返回// packages/preact-query/src/infiniteQueryOptions.ts export function infiniteQueryOptions(options: unknown) { return options }真正的价值全部集中在类型层面调用后返回的对象既保留了全部可用的配置字段又通过queryKey携带上已推断的数据类型标签让同一份 options 对象在不同 API 之间传递时不会丢失类型信息。正因如此它非常适合在以下场景使用把无限查询的配置从组件中提取出来与useInfiniteQuery解耦便于集中维护同一配置同时供 Hook 与queryClient.infiniteQuery预取、prefetchInfiniteQuery等命令式入口复用配合useSuspenseInfiniteQuery做数据预取或将配置直接交给预取辅助函数。其定义位于 packages/preact-query/src/infiniteQueryOptions.ts相关的 API 文档见 docs/framework/preact/reference/functions/infiniteQueryOptions.md。与queryOptions的对应关系如果你熟悉queryOptions普通查询的同类工具见 packages/preact-query/src/queryOptions.ts可以把infiniteQueryOptions理解为它在无限查询领域的孪生版本。区别在于普通查询的数据形态是单页数据无限查询的数据被包成InfiniteDataTData, TPageParam见下方说明无限查询额外要求initialPageParam与getNextPageParam等分页相关配置无限查询的配置类型参数多出一个TPageParam用于描述每次翻页传给queryFn的参数。三个重载与类型参数全解infiniteQueryOptions在类型层面定义了三个重载分别应对「是否设置了initialData」「queryFn是否允许为skipToken」等不同形态从而在编译期为每种用法推导出最精确的返回类型。文档中给出的签名如下function infiniteQueryOptionsTQueryFnData, TError, TData, TQueryKey, TPageParam(options): UseInfiniteQueryOptions... object QueryKeyWithDataTagTQueryKey, InfiniteDataTQueryFnData, unknown, TError重载位置触发条件options 参数类型关键约束第一个设置了initialDataDefinedInitialDataInfiniteOptionsinitialData必填data永不为undefined第二个未设置initialData且queryFn不能是skipTokenUnusedSkipTokenInfiniteOptionsqueryFn排除了SkipToken第三个未设置initialData最通用UndefinedInitialDataInfiniteOptionsqueryFn可为普通函数数据可能处于pending三个重载的实现位置分别在 infiniteQueryOptions.ts 的第 171、233、295 行附近对应的 options 类型别名文档可分别参阅 DefinedInitialDataInfiniteOptions、UnusedSkipTokenInfiniteOptions 与 UndefinedInitialDataInfiniteOptions。五个泛型参数逐一说明类型参数默认值含义TQueryFnData无必推单页数据的类型即你的queryFn解析出的结果类型TErrorDefaultError默认即ErrorqueryFn可能抛出的错误类型TDataInfiniteDataTQueryFnData, unknown经select处理后data的最终类型默认是「所有已抓取页面 页码参数」的聚合形态TQueryKeyreadonly unknown[]即QueryKey查询键的类型TPageParamunknown传给queryFn用于抓取某一页的页码参数类型其中TData的默认形态InfiniteData在 packages/query-core/src/types.ts 中定义得非常直观export interface InfiniteDataTData, TPageParam unknown { pages: ArrayTData pageParams: ArrayTPageParam }也就是说无限查询的data永远由「页面内容数组pages」和「每次翻页用到的参数数组pageParams」两部分组成pageParams中记录了每一页分别是用哪个参数抓回来的二者按序一一对应。返回值的类型标记无论命中哪个重载函数都返回同一个 options 对象只是返回类型中叠加了QueryKeyWithDataTagTQueryKey, InfiniteDataTQueryFnData, unknown, TError让queryKey携带数据与错误类型标记。QueryKeyWithDataTag在 query-core/src/types.ts 中的定义是export type QueryKeyWithDataTag TQueryKey extends QueryKey QueryKey, TQueryFnData unknown, TError DefaultError, { queryKey: DataTagTQueryKey, TQueryFnData, TError }DataTag通过特殊符号把「该查询键对应什么数据、什么错误」烙在queryKey的类型上。这样一来当同一份 options 被传入queryClient的缓存读取类方法时编译器也能从 query key 直接反推出data与error的类型而不需要再手动补泛型——这正是「配置定义一次、处处类型安全」的核心机制。快速上手把配置抽到组件外官方文档提供的第一个示例展示了最基本的用法把无限查询配置定义成模块级的projectsOptions再原样交给useInfiniteQueryimport { infiniteQueryOptions, useInfiniteQuery } from tanstack/preact-query export const projectsOptions infiniteQueryOptions({ queryKey: [projects], queryFn: ({ pageParam }) fetchProjects(pageParam), initialPageParam: 0, getNextPageParam: (lastPage) lastPage.nextId, initialData: { pages: [], pageParams: [] }, }) function Projects() { // data is never undefined, thanks to initialData — even if a refetch fails, so the // list stays visible alongside the error. const { data, isError, error } useInfiniteQuery(projectsOptions) return ( div {isError ? spanError: {error.message}/span : null} ul {data.pages.map((page) page.projects.map((p) li key{p.id}{p.name}/li))} /ul /div ) }要点解读queryKey是必填项它是生成配置的依据initialPageParam: 0定义了第一页使用的页码参数getNextPageParam: (lastPage) lastPage.nextId从上一页结果中取出下一页参数供下一次抓取使用该示例命中了「设置initialData」的重载所以data类型为DefinedInitialDataInfiniteOptions对应的结果类型——即便重抓失败列表仍会随错误一起保持可见。useInfiniteQuery的完整文档见 docs/framework/preact/reference/functions/useInfiniteQuery.md其实现位于 packages/preact-query/src/useInfiniteQuery.ts。从实现注释可以确认它能接受infiniteQueryOptions生成的配置对象并返回比普通useQuery更丰富的字段除data.pages、data.pageParams外还包含fetchNextPage、fetchPreviousPage、hasNextPage、hasPreviousPage、isFetchingNextPage、isFetchingPreviousPage等。设置initialData的重载让data永不为空第一个重载要求initialData存在。它对应的 options 类型是DefinedInitialDataInfiniteOptions其中的initialData有三种合法写法见 infiniteQueryOptions.tsinitialData: | NonUndefinedGuardInfiniteDataTQueryFnData, TPageParam | (() NonUndefinedGuardInfiniteDataTQueryFnData, TPageParam) | undefined源码注释同样保留在 DefinedInitialDataInfiniteOptions 文档中对其语义作了精确说明initialData会被用作查询缓存的初始数据——前提是该查询尚未被创建或尚未有缓存如果传入的是函数函数只会在共享/根查询初始化期间被调用一次且必须同步返回初始数据initialData默认被视为过期数据stale除非设置了staleTimeinitialData会持久化写入缓存。也就是说它不是组件挂载后为了“好看”临时塞进渲染层的占位符而是会真实参与缓存与过期判断的真实数据。设置initialData后类型层面即可保证data不会是undefined这也是它与「无initialData重载」最本质的类型差异。未设置initialData的重载与skipToken的边界如果你没有设置initialData编译器会在剩余两个重载中继续区分queryFn是否允许是skipToken。最通用的第三个重载UndefinedInitialDataInfiniteOptions对应标准形态查询处于pending时data可能是undefined因此组件里通常要先用isPending做加载判断。值得注意的是第二个重载UnusedSkipTokenInfiniteOptions它排除了skipToken作为queryFn的可能并借助OmitKeyof..., queryFn对queryFn字段做了收紧。为什么这样设计类型定义中给出的原话值得逐句细读见 infiniteQueryOptions.tsskipTokenis not allowed as a value here — this overload is selected when noinitialDatais set. If you dont intend to run the query yet, setenabled: false— omittingqueryFnalone still triggers a fetch that fails with Missing queryFn unlessenabledisfalseor a default query function has been defined.翻译成实践结论在无限查询语境下skipToken与initialData不应同时使用二者走的是不同的重载分支如果暂时不想发起请求正确做法是设置enabled: false而不是省略queryFn——省略queryFn仍会触发抓取并因 “Missing queryFn” 失败除非enabled为false即便定义了默认查询函数default query function它也只会补上queryFn本身不会推迟请求的发起。参数化工厂模式一份工厂多个业务键复用对于评论列表这类「每个业务实体如postId都有一套独立无限查询」的场景文档给出了参数化工厂的推荐写法。它实际上返回一个函数每次传入不同的postId就生成一份带独立queryKey的 optionsimport { infiniteQueryOptions, useInfiniteQuery } from tanstack/preact-query export const commentsOptions (postId: string) infiniteQueryOptions({ queryKey: [post, postId, comments], queryFn: ({ pageParam }) fetchComments(postId, pageParam), initialPageParam: 0, getNextPageParam: (lastPage) lastPage.nextId, }) function Comments({ postId }: { postId: string }) { const { data, isPending, isError, error } useInfiniteQuery(commentsOptions(postId)) if (isPending) return Loading... if (isError) return spanError: {error.message}/span return ( ul {data.pages.map((page) page.comments.map((c) li key{c.id}{c.text}/li))} /ul ) }该模式的核心收益每个postId拥有以[post, postId, comments]为键的独立缓存互不串扰配置集中在一处定义若后续要为评论增加预取只需把commentsOptions(postId)同样传给预取 API 即可由于 query key 携带了DataTag跨 API 复用时数据/错误类型可自动推断。useInfiniteQuery还支持用skipToken暂时禁用查询直到postId就绪详细可参见 useInfiniteQuery 文档中的相关示例。跨 Hook 与命令式 API 复用预取与 SuspenseinfiniteQueryOptions文档反复强调同一份 options 可以被 Hook 与命令式 API 共享。仓库中preact-query提供了现成的消费方全部接受该工厂生成的配置对象1.usePrefetchInfiniteQuery——见 packages/preact-query/src/usePrefetchInfiniteQuery.tsx 与 usePrefetchInfiniteQuery 文档。它专门用来在 Suspense 边界之前、渲染期间发起预取本身不返回任何值。实现上有一个值得注意的细节它会先检查client.getQueryState(options.queryKey)只有查询没有任何缓存状态包括上次遗留的pending/error状态时才执行client.infiniteQuery(options)所以即便每次渲染都调用它也不会重复抓取已有或进行中的数据const client useQueryClient(queryClient) if (!client.getQueryState(options.queryKey)) { void client.infiniteQuery(options).catch(noop) }其文档示例正是直接接收infiniteQueryOptions生成的projectsOptionsimport { Suspense } from preact/compat import { infiniteQueryOptions, usePrefetchInfiniteQuery } from tanstack/preact-query const projectsOptions infiniteQueryOptions({ queryKey: [projects], queryFn: ({ pageParam }) fetchProjects(pageParam), initialPageParam: 0, getNextPageParam: (lastPage) lastPage.nextId, }) function App() { // Fire the prefetch during render, before the suspense boundary below. usePrefetchInfiniteQuery(projectsOptions) return ( Suspense fallback{h1Loading projects.../h1} Projects / /Suspense ) }注意预取场景的约束queryKey、initialPageParam、getNextPageParam始终必填queryFn在未定义默认查询函数时也必填且queryFn不允许为skipToken见 types.ts 中UsePrefetchInfiniteQueryOptions的定义。2.useSuspenseInfiniteQuery——Suspense 形态的无限查询 Hook文档见 docs/framework/preact/reference/functions/useSuspenseInfiniteQuery.md。它的 options 类型UseSuspenseInfiniteQueryOptions与UseInfiniteQueryOptions几乎一致只是剔除了enabled、throwOnError、placeholderDataSuspense Hook 无法渲染“禁用”或“占位”状态且同样不允许queryFn为skipToken见 types.ts。3.queryClient.infiniteQuery等命令式入口——由于类型层面共享任何拿到infiniteQueryOptions(...)返回值的地方都可以把它作为参数传给命令式 API如queryClient.prefetchInfiniteQuery/infiniteQuery。需要留意官方在 useInfiniteQuery.ts 中给出的提醒命令式发起的fetchNextPage等调用可能与默认的自动重抓行为相互干扰导致数据过期因此应当只在用户交互回调中调用或配合hasNextPage !isFetching这样的条件。底层原理页码参数在无限查询中如何流转infiniteQueryOptions本身不执行抓取它只是把分页策略声明成数据。真正的执行逻辑在 query-core 中理解它有助于你正确书写getNextPageParam。从 packages/query-core/src/types.ts 可以看到GetNextPageParamFunction的完整签名export type GetNextPageParamFunctionTPageParam, TQueryFnData unknown ( lastPage: TQueryFnData, allPages: ArrayTQueryFnData, lastPageParam: TPageParam, allPageParams: ArrayTPageParam, ) TPageParam | undefined | null它最多接收四个实参最后一页数据、全部页面数据、最后一个页码参数、全部页码参数返回值若为undefined或null则代表没有下一页。InfiniteData.pages与.pageParams正是由这类函数逐页驱动填充的。packages/query-core/src/infiniteQueryBehavior.ts 负责把这些声明转换为实际抓取行为其中首屏会使用initialPageParam在无已有页码参数时兜底取oldPageParams[0] ?? options.initialPageParam抓取单页时会把pageParam写入queryFn的执行上下文context.fetchFngetNextPageParam与hasNextPage协作决定是否还有下一页。这些行为被大量测试覆盖例如 packages/query-core/src/tests/infiniteQueryObserver.test.tsx 中验证了getNextPageParam返回undefined或null时停止继续抓取下一页、initialPageParam为null时也能正常抓取首页、以及页面为空时不调用getNextPageParam等边界情况。类型层面的约束同样有类型测试支撑见 packages/query-core/src/tests/infiniteQueryObserver.test-d.tsx例如「不传getNextPageParam时不允许再传pages」。总结何时使用infiniteQueryOptions多个消费方需要共享同一份无限查询配置组件内 Hook 渲染期预取 命令式预取时务必使用infiniteQueryOptions封装一次而不是在 Hook 里内联重复书写依赖查询键推断数据类型的场景返回对象带DataTag标记能让queryClient的缓存读写免去手写泛型需要data永不 undefined时选择带initialData的重载但要清楚它会以 stale 状态写入缓存暂时不发起请求时不要用省略queryFn的方式应显式设置enabled: false若仅在某一个组件内部使用且无需预取/命令式共享直接内联写useInfiniteQuery({...})也完全合法——infiniteQueryOptions的价值在于复用而非强制。实现层面它的运行时仅是恒等返回、全部智能由类型系统承载因此把它当作纯编译期工具来理解即可一份配置、处处复用、处处类型安全。【免费下载链接】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),仅供参考