深入解析 Preact Query 的 DefinedInitialDataInfiniteOptions让无限查询的 data 永不为 undefined【免费下载链接】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 适配层tanstack/preact-query中一个关键的类型别名——DefinedInitialDataInfiniteOptions系统拆解它在infiniteQueryOptions/useInfiniteQuery重载体系中的位置、initialData的完整运行语义函数调用一次、默认视为 stale、写入缓存以及五个泛型参数各自的作用。读完本文你将能理解 Preact Query 是如何通过“是否设置了initialData”在类型层面区分“data 可能为 undefined”与“data 一定存在”两种无限查询形态并据此编写出无需空值判断、类型安全的分页列表组件。概览一个用于“无限查询 预置初始数据”的类型别名在 Preact Query 中无限查询infinite query用于加载分页 / 无限滚动数据其选项通常经由 infiniteQueryOptions 工厂函数 创建再传递给useInfiniteQuery或命令式 API如queryClient.infiniteQuery共享使用。DefinedInitialDataInfiniteOptions正是infiniteQueryOptions在设置了initialData时所命中的那个重载overload接受的选项类型。其类型方程式为type DefinedInitialDataInfiniteOptionsTQueryFnData, TError, TData, TQueryKey, TPageParam UseInfiniteQueryOptionsTQueryFnData, TError, TData, TQueryKey, TPageParam object;它本质上是 UseInfiniteQueryOptions即useInfiniteQuery的全部选项与一个约束initialData的附加对象的交叉类型源码定义于 packages/preact-query/src/infiniteQueryOptions.ts:104。该类型带来的最直接收益当设置initialData后useInfiniteQuery的返回值类型也会被切换到DefinedUseInfiniteQueryResult参见 DefinedUseInfiniteQueryResult从而在编译期保证data不会是undefined渲染阶段无需再做空值收窄。重载家族为什么需要三个并列的类型别名要理解DefinedInitialDataInfiniteOptions最好先看它所在的“三兄弟”结构。在 packages/preact-query/src/infiniteQueryOptions.ts 中infiniteQueryOptions函数声明了三个重载分别对应三种选项类型类型别名initialData形态data类型是否允许queryFn为skipTokenDefinedInitialDataInfiniteOptions已设置值或返回值的函数永不undefined允许UnusedSkipTokenInfiniteOptions未设置pending 期间可能为undefined不允许UndefinedInitialDataInfiniteOptions未设置pending 期间可能为undefined允许三个重载在 infiniteQueryOptions.ts:171、infiniteQueryOptions.ts:233 与 infiniteQueryOptions.ts:295 处定义。运行时实现极其简单——export function infiniteQueryOptions(options: unknown) { return options }见 infiniteQueryOptions.ts:318即原样返回对象因此这些重载纯属“编译期标签”不会带来任何运行时开销。它们唯一的工作是把queryKey打上QueryKeyWithDataTagTQueryKey, InfiniteDataTQueryFnData, TError类型标签让后续调用方能从 key 中推导出数据的类型。从源码结构可以推断TypeScript 重载决议遵循“从上到下取第一个可匹配”的规则因此当你传入带有initialData的选项对象时会命中第一个Defined重载data被声明为非undefined这也是此类型别名“被选择”的含义所在。initialData类型声明逐行拆解该类型的核心约束集中在initialData字段上定义见 infiniteQueryOptions.ts:124-127完整类型声明如下initialData: | NonUndefinedGuardInfiniteDataTQueryFnData, TPageParam | (() NonUndefinedGuardInfiniteDataTQueryFnData, TPageParam) | undefined;与未设置initialData的 UndefinedInitialDataInfiniteOptions其中initialData是可选项initialData?不同此处的initialData不带可选问号——一旦选择该重载就必须显式提供该字段。它的取值可以是三类NonUndefinedGuardInfiniteDataTQueryFnData, TPageParam直接的初始数据值一个同步返回上述数据的函数() NonUndefinedGuard...undefined本身保持类型闭包的完整性。其中NonUndefinedGuardT定义于 packages/query-core/src/types.ts:12本质是一个递归剔除工具类型export type NonUndefinedGuardT T extends undefined ? never : T四条约定的运行语义依据类型声明注释及query-core的既有测试initialData的行为可以概括为四条核心约定写入缓存的前提该值会被用作查询缓存的初始数据但仅当该查询尚未被创建或尚未有缓存时才会生效即查询一旦已存在缓存数据initialData不会覆盖它。函数只调用一次若传入函数该函数只会在共享/根查询shared/root query初始化期间被调用一次并且必须同步返回初始数据不支持返回 Promise。query-core 中的测试 query.test.tsx:1299「should call initialData function when it is a function」即验证了该函数恰好被调用一次expect(initialDataFn).toHaveBeenCalledTimes(1)。默认视为 stale初始数据默认被认为是过期的stale因此查询创建后往往会立即触发后台重新获取除非你在选项中同时设置了staleTime使数据在指定时间内保持 fresh 而跳过首轮请求。query-core 的测试 queryClient.test.tsx:590「should not fetch when initialData is provided」则从另一面验证当数据仍 fresh 时不会发起请求。会写入缓存initialData会被持久化到缓存中。这与placeholderData占位数据形成鲜明对比——后者同样让查询“看起来有数据”但不会写入缓存。官方指南 initial-query-data.md 明确建议不要把占位的、部分的、不完整的数据放进initialData因为它会被缓存这类场景应改用placeholderData参见 placeholder-query-data.md其明确说明“数据不会持久化到缓存”。五个类型参数逐一解析同UseInfiniteQueryOptions一致DefinedInitialDataInfiniteOptions携带五个泛型参数全部有默认值因此多数情况下无需手动指定交给 TypeScript 推断即可TQueryFnData无默认值必须由调用方推断。表示单个页面的数据类型即你的queryFn解析返回的数据形状。在无限查询中它对应InfiniteData中pages数组的元素类型。TError默认值为DefaultError。表示queryFn可能抛出的错误类型。DefaultError定义于 packages/query-core/src/types.ts:45-49它是一个受Register接口驱动的可扩展类型默认解析为Error但允许应用通过模块扩展Register[defaultError]来自定义全局错误类型export type DefaultError Register extends { defaultError: infer TError } ? TError : ErrorTData默认值为InfiniteDataTQueryFnData。表示data在select变换之后最终呈现的类型——如果你没有使用select它就是InfiniteDataTQueryFnData即所有已获取页面与它们的分页参数拼成的容器。InfiniteDataTData, TPageParam unknown结构定义于 packages/query-core/src/types.ts:210-213export interface InfiniteDataTData, TPageParam unknown { pages: ArrayTData pageParams: ArrayTPageParam }也就是说一个无限查询的data由两个平行数组组成data.pages已加载的各页数据与data.pageParams每页对应的分页参数通常用于后续重新加载或缓存还原。TQueryKey约束为TQueryKey extends QueryKey默认值QueryKey。表示queryKey的类型。QueryKey同样定义于 packages/query-core/src/types.ts:51-59同样可被Register[queryKey]扩展覆盖本质是一个ReadonlyArrayunknown。TPageParam默认值为unknown。表示传给queryFn以获取某一页的参数类型。在你的场景中通常是数字页码或游标字符串如nextId。当initialData传入时InfiniteDataTQueryFnData, TPageParam会把该类型带入初始值校验从而保证初始pageParams的元素类型与运行时传入queryFn的pageParam完全一致。下游协作从选项类型到“data 一定存在”的结果类型DefinedInitialDataInfiniteOptions并非孤立存在它与infiniteQueryOptions、useInfiniteQuery以及结果类型构成一条完整的类型链调用infiniteQueryOptions({ ..., initialData })命中 Defined 重载返回类型为DefinedInitialDataInfiniteOptions QueryKeyWithDataTag...把该选项传给 useInfiniteQuery其 Defined 重载useInfiniteQuery.ts:64-79要求DefinedInitialDataInfiniteOptions类型的入参并返回DefinedUseInfiniteQueryResultTData, TErrorDefinedUseInfiniteQueryResult是 query-core 中DefinedInfiniteQueryObserverResult的再导出见 types.ts:376其结果中的data类型为TData而非TData | undefined。运行时的调用链在 useInfiniteQuery.ts:361-369真正的实现只是把选项交给useBaseQuery(options, InfiniteQueryObserver, queryClient)由 query-core 的InfiniteQueryObserver完成状态机与缓存逻辑。因此所有类型分支都发生在编译期运行时行为与普通useInfiniteQuery完全一致。实战预置空列表错误刷新时数据依然可见将以上类型特性落到实际组件中最典型的场景是给无限查询预置一个空列表作为初始数据从而让用户在任何时刻都看到已渲染的列表结构即便后台重新拉取第一页失败data依然存在页面可以与错误提示共存。下面这个示例来自 infiniteQueryOptions.ts:143-169 的 JSDocimport { 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 永不等于 undefined —— 即使重试失败列表仍可显示错误与数据并存 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 ) }注意这里initialData: { pages: [], pageParams: [] }的类型完全符合InfiniteData的结构初始没有页面、没有分页参数但data从第一次渲染起就是一个真实对象。因此解构出的data.pages可直接.map(...)无需data?.pages这样的空值保护同时类型系统要求initialPageParam、getNextPageParam一并提供分页参数与初始空列表的类型保持自洽。作为对比若不设置initialData则必须使用UndefinedInitialDataInfiniteOptions形态组件里通常需要先处理isPendingfunction Projects() { const { data, isPending, isError, error, fetchNextPage, hasNextPage, isFetching } useInfiniteQuery({ queryKey: [projects], queryFn: ({ pageParam }) fetchProjects(pageParam), initialPageParam: 0, getNextPageParam: (lastPage) lastPage.nextId, }) 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} {hasNextPage ? Load More : Nothing more to load} /button / ) }两种写法的取舍很清晰需要“首屏即渲染、即便失败也保留列表”就选initialDataDefined 形态希望首屏展示 Loading、数据真正到位后再渲染则使用默认Undefined形态。此外若你暂不打算发起查询又想保留类型安全可采用skipToken作为queryFn此时将命中UnusedSkipTokenInfiniteOptions而不是依赖省略queryFn的行为——后者仍会触发一次以“Missing queryFn”失败的请求详见 infiniteQueryOptions.ts:76-90 的注释说明。总结DefinedInitialDataInfiniteOptions是 Preact Query 类型系统中“用数据形态换取类型确定性”的代表性设计通过initialData是否设置这一运行时开关在编译期把无限查询划分为data永不undefined与可能undefined的两类并联动useInfiniteQuery的返回类型与queryKey的数据标签QueryKeyWithDataTag让开发者享受接近完全的类型推断与空值安全。理解它的五个泛型参数与NonUndefinedGuard、InfiniteData等底层类型的配合就能在编写分页与无限滚动组件时准确选择 Defined / Undefined / SkipToken 三种形态写出更少防御、更多确定性的 Preact Query 代码。想要进一步实践可参考仓库内 useInfiniteQuery 参考文档含按钮加载更多、滚动触发IntersectionObserver自动加载等完整示例与 无限查询指南并在 packages/query-core/src/tests中阅读关于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创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考 SEO 优化官网定制响应式建站教育培训建站