ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

深入解析 Preact Query 的 UndefinedInitialDataInfiniteOptions:无 initialData 无限查询的类型契约与缓存语义

深入解析 Preact Query 的 UndefinedInitialDataInfiniteOptions:无 initialData 无限查询的类型契约与缓存语义 深入解析 Preact Query 的 UndefinedInitialDataInfiniteOptions无 initialData 无限查询的类型契约与缓存语义【免费下载链接】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 仓库中tanstack/preact-query包的公开类型别名UndefinedInitialDataInfiniteOptions展开。它描述的是「未提供initialData时」infiniteQueryOptions(...)重载所接受的完整选项集合其核心语义是在查询处于pending状态期间data的值可能为undefined。读完本文你将理解该类型的定义位置、五个泛型参数的约束、它与DefinedInitialDataInfiniteOptions、UnusedSkipTokenInfiniteOptions两个兄弟类型的分工关系以及initialData在底层缓存中的真实行为——从而能在 Preact 应用中写出类型精确、分页行为可预期的无限滚动查询代码。一、该类型在代码库中的位置该类型是官方文档 type-aliases/UndefinedInitialDataInfiniteOptions 的讲解对象其声明定义在 packages/preact-query/src/infiniteQueryOptions.tsexport type UndefinedInitialDataInfiniteOptions TQueryFnData, TError DefaultError, TData InfiniteDataTQueryFnData, TQueryKey extends QueryKey QueryKey, TPageParam unknown, UseInfiniteQueryOptions TQueryFnData, TError, TData, TQueryKey, TPageParam { initialData?: | undefined | NonUndefinedGuardInfiniteDataTQueryFnData, TPageParam | InitialDataFunction NonUndefinedGuardInfiniteDataTQueryFnData, TPageParam }从源码结构看该类型有两个显著特征它整体建立在UseInfiniteQueryOptions之上定义见 packages/preact-query/src/types.ts只是额外追加了对initialData字段的约束。也就是说useInfiniteQuery能接受的一切配置——queryKey、queryFn、initialPageParam、getNextPageParam、getPreviousPageParam、maxPages、select、staleTime、gcTime等——它都能接受。initialData被定义为可选的initialData?且允许显式传入undefined。这正是「未设置initialData」这一重载分支的本质类型层面不再保证data一定存在。二、它解决的问题用重载体系消除「data 可能为 undefined」的类型隐患infiniteQueryOptions是 Preact Query 提供的、用于把无限查询选项从组件内提升为可复用「选项工厂」的辅助函数。为了让消费者在编译期就能感知data是否始终有值它基于选项内容声明了三个重载见同一文件 infiniteQueryOptions.ts重载分支选用条件对应选项类型data 是否为 undefined设置了initialDatainitialData为必填DefinedInitialDataInfiniteOptions绝不 undefined未设置initialData、queryFn不是skipTokeninitialData可缺省queryFn排除skipTokenUnusedSkipTokenInfiniteOptions可能 undefined未设置initialData兜底initialData允许为undefinedUndefinedInitialDataInfiniteOptions可能 undefinedpending 期间TypeScript 按声明顺序尝试匹配重载传入对象含initialData时命中第一个分支不含initialData且queryFn非skipToken时命中第二个其余情形例如queryFn使用了skipToken落入UndefinedInitialDataInfiniteOptions兜底分支。因此本文主角是文档与类型双重视角下的「无initialData场景标准形态」。当一个无initialData的调用命中该分支后infiniteQueryOptions的返回类型是UndefinedInitialDataInfiniteOptionsTQueryFnData, TError, TData, TQueryKey, TPageParam QueryKeyWithDataTagTQueryKey, InfiniteDataTQueryFnData, TError即原选项对象原样返回同时叠加QueryKeyWithDataTag——它在queryKey上携带「该 key 对应数据类型为InfiniteDataTQueryFnData」的标记QueryKeyWithDataTag定义于 packages/query-core/src/types.ts使queryClient.getQueryData(options.queryKey)等命令式 API 在后续使用中能够自动获得类型推断无需手动补泛型。三、initialData 字段类型形态与三种取值该类型追加的字段声明为联合类型可归为三类取值initialData?: | undefined | NonUndefinedGuardInfiniteDataTQueryFnData, TPageParam | InitialDataFunctionNonUndefinedGuardInfiniteDataTQueryFnData, TPageParam取值说明undefined不提供任何初始数据。这是「无 initialData」分支的默认形态查询进入pendingdata为undefinedNonUndefinedGuardInfiniteData...一个完整的无限数据对象。NonUndefinedGuardT在 query-core 中定义为T extends undefined ? never : T用于从类型上排除undefined保证你给的是一个真正的InfiniteData结构而非空值InitialDataFunction...返回上述对象的惰性函数。InitialDataFunctionT定义为() T | undefined见 query-core/types.tsInfiniteData 的具体形态无论是直接值还是函数返回值initialData都必须满足无限查询的InfiniteDataTData, TPageParam结构定义见 query-core/src/types.tsexport interface InfiniteDataTData, TPageParam unknown { pages: ArrayTData pageParams: ArrayTPageParam }即pages是已抓取页的数组pageParams是与每页一一对应的分页参数数组。例如一个空列表的初始数据必须写成{ pages: [], pageParams: [] }而不是只给一个空数组。之所以是「每页数据 每页参数」成对存储是因为无限查询需要借助pageParams精确重建每一页的请求参数它记录了每页来自哪个pageParam。四、五个泛型参数逐一解读与UseInfiniteQueryOptions保持一致该类型携带五个泛型参数其中四个有默认值TQueryFnData必填queryFn解析出的单个页面的数据类型。在无限查询里queryFn每次只负责抓取一页因此该泛型描述的是「一页」而非「全部页」。TError默认 DefaultErrorqueryFn可能抛出的错误类型默认DefaultError即unknown的别名体系。若你的queryFn会抛自定义错误如HttpError应显式传入以便error字段获得精确类型。TData默认 InfiniteData data在select变换之后最终呈现的类型。默认值为InfiniteDataTQueryFnData——即全部已抓取页与它们的页参数所构成的结构。如果你用select对分页数据做了投影例如只保留部分字段则应相应调整此参数。TQueryKey默认 QueryKeyqueryKey的类型默认QueryKey。推荐使用模板字面量类型或元组字面量如[post, postId, comments]以获得更细粒度的 key 类型。TPageParam默认 unknown传给queryFn以抓取某一页的参数类型。实践中通常与initialPageParam、getNextPageParam的返回类型一致例如number表示页码、string表示游标。五、运行期语义initialData 如何进入缓存类型之上需要理解initialData在运行期的真实行为。官方文档对本字段的三条关键说明在源码中都有印证对应 infiniteQueryOptions.ts 的 JSDoc1. 只要查询尚未创建或未被缓存该值就会被用作查询缓存的初始数据。在 packages/query-core/src/query.ts 的getDefaultState中构建查询默认状态时会直接消费initialDataconst data typeof options.initialData function ? (options.initialData as InitialDataFunctionTData)() : options.initialData const hasData data ! undefined // ... status: hasData ? success : pending,可以看到只要initialData解析结果非undefined查询的初始status就是success、dataUpdateCount为 0一旦查询已存在于缓存中该值不会被再次覆盖。2. 若传函数该函数在共享/根查询初始化期间只会被调用一次且必须同步返回初始数据。这点从类型与实现上双重保证InitialDataFunction是同步函数签名() T | undefined调用发生在查询对象构建默认状态的同步路径上getDefaultState因此不允许异步。这也是它与「每次渲染都会评估」的placeholderData函数最大的不同。3.initialData会持久化进缓存默认视为 stale除非设置了staleTime。因为该值直接写入了QueryState而不仅是展示用占位所以initialData与placeholderData有本质区别——后者不会进入缓存、不参与持久化、发生后台重取失败时会被丢弃。关于默认 stale 的行为在getDefaultState中未显式提供initialDataUpdatedAt时dataUpdatedAt被置为Date.now()在默认staleTime: 0下观察者挂载后该数据即刻过期从而触发后台重取。若设置了staleTime则在对应时间窗口内数据被视为新鲜、直接走缓存。若需要自定义「这份初始数据在何时产生」的时间戳例如来自 SSR 序列化的时间可用initialDataUpdatedAt配合调整。六、实战未提供 initialData 的无限查询写法当不使用initialData时UndefinedInitialDataInfiniteOptions语义下的完整场景——data在pending期间为undefined——需要通过显式状态分支来处理。以下是从 useInfiniteQuery.ts 源码示例整理出的「Load More」按钮标准写法import { infiniteQueryOptions, useInfiniteQuery } from tanstack/preact-query export const projectsOptions infiniteQueryOptions({ queryKey: [projects], queryFn: ({ pageParam }) fetchProjects(pageParam), initialPageParam: 0, getNextPageParam: (lastPage) lastPage.nextId, }) function Projects() { const { data, isPending, isError, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, } useInfiniteQuery(projectsOptions) // 由于未设置 initialDatadata 在 pending 期间为 undefined // 必须先用 isPending / isError 收窄类型后再访问 data.pages。 if (isPending) return Loading... if (isError) return spanError: {error.message}/span return ( ul {data.pages.map((page) page.projects.map((project) li key{project.id}{project.name}/li), )} /ul button onClick{() fetchNextPage()} disabled{!hasNextPage || isFetching} {isFetchingNextPage ? Loading more... : hasNextPage ? Load More : Nothing more to load} /button / ) }要点projectsOptions被单独声明并导出可同时交给useInfiniteQuery与命令式 API如queryClient.infiniteQuery复用组件内通过isPending先行短路之后 TS 才能将data收窄为非空这正是「UndefinedInitialDataInfiniteOptions下 data 可能为 undefined」在编码上的直接体现fetchNextPage属于命令式抓取可能干扰默认的重取行为应只在用户动作回调里调用或用hasNextPage !isFetching这类条件做防护源码 JSDoc 有明确提醒见 useInfiniteQuery.ts。若你的组件在postId尚为空时需要先「禁用」无限查询可把queryFn设为skipToken而不要依赖省略queryFn此时走的就是UndefinedInitialDataInfiniteOptions兜底分支并用isLoading而非isPending区分禁用状态与真实加载示例见 useInfiniteQuery.ts。七、与 DefinedInitialDataInfiniteOptions 的取舍对比是否提供initialData直接决定了消费方代码的安全感与冗余度典型对照场景见 infiniteQueryOptions.ts 的 JSDoc 示例// 设置 initialDatadata 永不 undefined export const projectsOptions infiniteQueryOptions({ queryKey: [projects], queryFn: ({ pageParam }) fetchProjects(pageParam), initialPageParam: 0, getNextPageParam: (lastPage) lastPage.nextId, initialData: { pages: [], pageParams: [] }, }) function Projects() { // 得益于 initialData即使后台重取失败旧列表也保持可见并伴随错误提示 // data 在类型上被保证永远有值无需 isPending 收窄。 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 ) }两条路线的取舍可以归纳为UndefinedInitialDataInfiniteOptions不设 initialData首屏真实地经历pending → success需要多写isPending分支但语义纯粹不会有「初始空数据被当作真实成功数据」的混淆适合首屏必须真实加载的场景。DefinedInitialDataInfiniteOptions设 initialData以「空列表也视为 success」为代价换来了data永不为undefined的类型保证即使刷新失败 UI 也能留存并同时展示错误适合需要立刻渲染骨架/空态、且能接受空数据占位的场景。八、小结面向搜索引擎与类型使用者的速查定义位置UndefinedInitialDataInfiniteOptions声明于 packages/preact-query/src/infiniteQueryOptions.ts官方类型说明见 docs/framework/preact/reference/type-aliases/UndefinedInitialDataInfiniteOptions.md。本质UseInfiniteQueryOptions交集{ initialData?: undefined | InfiniteData | (() InfiniteData) }即「未设置 initialData」时infiniteQueryOptions各重载选用的选项类型此时pending期间data可为undefined。initialData 三条硬规则查询尚未创建/缓存时生效函数形态只在初始化期间同步调用一次数据会持久化进缓存默认 stale除非配置staleTime。相邻类型DefinedInitialDataInfiniteOptionsinitialData 必填data 恒有值、UnusedSkipTokenInfiniteOptions无 initialData 且 queryFn 禁用 skipToken、UndefinedInitialDataOptions普通查询版对应类型。借助这一类型体系Preact 开发者可以在编译期就把「数据是否一定有值」这一业务前提固化下来从根本上避免data.pages空值访问这类运行时错误——这正是 TanStack 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),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进