Preact Query 中UseInfiniteQueryResult返回值类型详解:useInfiniteQuery 的结果类型与空数据状态分析
【免费下载链接】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
UseInfiniteQueryResult是 TanStack Query 在 Preact 框架适配层(@tanstack/preact-query)中为useInfiniteQueryHook 提供的基础返回值类型。它本身是一个类型别名,直接重导出(re-export)自@tanstack/query-core的InfiniteQueryObserverResult,用于描述「未设置initialData」时无限查询从 pending、error 到 success 全生命周期的结果形态。读完本文,你将掌握该类型的精确定义位置与结构、data在何种场景下为undefined、两个类型参数的含义与推导方式,以及它与DefinedUseInfiniteQueryResult、UseQueryResult等相关类型的区别,并能在 Preact 应用中安全地消费无限分页数据。
一、类型定义:一行别名的背后是整套无限查询结果协议
在 packages/preact-query/src/types.ts 中,该类型别名被定义在第 364~367 行:
/** * The result of `useInfiniteQuery` when `initialData` isn't set — `data` may be `undefined` while the query is * `pending`. Re-exports {@link InfiniteQueryObserverResult} from `@tanstack/query-core`. */ export type UseInfiniteQueryResult< TData = unknown, TError = DefaultError, > = InfiniteQueryObserverResult<TData, TError>可以看到,Preact 适配层并没有为无限查询重新设计一套结果类型,而是把「骨架」(InfiniteQueryObserverResult)留在与框架无关的核心包@tanstack/query-core中,再由每个框架包以自己的名字做一次薄薄的转写:
- React 版本对应
UseInfiniteQueryResult(见packages/react-query中的同名别名); - Preact 版本即本文讨论的 UseInfiniteQueryResult.md;
- 核心层定义见 packages/query-core/src/types.ts 第 1058~1066 行的
InfiniteQueryObserverResult。
这种「核心定义 + 框架转写」的架构是 TanStack Query 多框架同构设计的典型体现:useInfiniteQuery、useQuery等 Hook 在不同框架下共享同一套 observer 与查询状态机,类型系统也随之保持框架间行为一致。
别名 vs 接口:为什么用type
该结果既可能是pending状态(data为undefined),又可能是success状态(data必然有值),还可能是带错误的重取(refetch error)状态。不同类型下同一字段的可空性完全不同,只有「可辨识联合类型」才能精确表达这种随status收缩的形态。因此实现采用type联合而非interface继承,这是保证类型收窄(type narrowing)能够生效的前提——interface无法做同样严格的字段冲突检查。
二、联合类型结构:五个状态分支如何刻画无限查询生命周期
InfiniteQueryObserverResult在 packages/query-core/src/types.ts 第 1058~1066 行的完整定义为:
export type InfiniteQueryObserverResult< TData = unknown, TError = DefaultError, > = | DefinedInfiniteQueryObserverResult<TData, TError> | InfiniteQueryObserverLoadingErrorResult<TData, TError> | InfiniteQueryObserverLoadingResult<TData, TError> | InfiniteQueryObserverPendingResult<TData, TError> | InfiniteQueryObserverPlaceholderResult<TData, TError>其中的五个分支(同文件各段定义)共同决定了data、error与各状态位(status、isPending、isSuccess等)的合法组合:
| 状态分支 | 位置 | data | error | status | isPending/isSuccess |
|---|---|---|---|---|---|
InfiniteQueryObserverPendingResult | types.ts#L946-L979 | undefined | null | 'pending' | isPending: true、isSuccess: false |
InfiniteQueryObserverLoadingResult | types.ts#L963-L979 | undefined | null | 'pending' | 同时满足isLoading: true |
InfiniteQueryObserverLoadingErrorResult | types.ts#L981-L997 | undefined | TError | 'error' | isLoadingError: true |
InfiniteQueryObserverSuccessResult | types.ts#L1015-L1031 | TData | null | 'success' | isSuccess: true |
InfiniteQueryObserverRefetchErrorResult | types.ts#L999-L1013 | TData | TError | 'error' | isRefetchError: true |
InfiniteQueryObserverPlaceholderResult | types.ts#L1033-L1049 | TData | null | 'success' | isPlaceholderData: true |
DefinedInfiniteQueryObserverResult则只由RefetchError与Success两个「已有数据」的分支组成(types.ts#L1051-L1056)。
为何「没有 initialData 时 data 可能是 undefined」
这正是本类型最核心的语义(官方文档也据此给出说明):当useInfiniteQuery未配置initialData时,首次加载期间查询处于pending状态,此时对应InfiniteQueryObserverLoadingResult/InfiniteQueryObserverPendingResult分支,其data被明确收窄为undefined。
由此得到一个重要的编程结论:在拿到结果后,必须先对status(或isPending/isError等布尔位)做判断,TypeScript 才会把data收窄为有值类型。如果直接编写data.pages.map(...),编译器会报错——这是类型系统刻意留给你的「安全护栏」,提醒你处理加载态,而不是运行时才炸出undefined访问错误。
三、类型参数:TData 与 TError
该别名接受两个泛型参数(默认值均与 query-core 保持一致):
TData(默认unknown)
「select执行之后data的最终类型」。无限查询的选项支持select变换,因此TData指的是变换产物在结果对象data.pages数组元素上的类型,而不是原始queryFn的原始返回类型。
TError(默认DefaultError)
「queryFn可能抛出的错误类型」。DefaultError在 query-core 中默认取Error,当你的queryFn抛出自定义错误(例如 API 客户端错误类)时,通过useInfiniteQuery的泛型参数把它传入,error字段与isError分支即可得到精确的类型收窄。
两者可同时通过 Hook 调用推导。useInfiniteQuery的函数签名(useInfiniteQuery.ts#L344-L359)实际带有 5 个泛型:TQueryFnData(queryFn 原始数据)、TError、TData(经 select 后)、TQueryKey、TPageParam。若TData未显式给定,默认推导为InfiniteData<TQueryFnData, unknown>——即一个{ pages: TQueryFnData[]; pageParams: unknown[] }形态的数据容器(InfiniteData定义见 packages/query-core/src/types.ts#L210-L213)。
四、姊妹类型:DefinedUseInfiniteQueryResult 与 useInfiniteQuery 的重载切换
UseInfiniteQueryResult只在「未设置initialData」时被使用;当设置了initialData,返回值类型会切换为DefinedUseInfiniteQueryResult——在该类型下data永远不为undefined,即使在重取失败时也有旧数据兜底。
两者在 packages/preact-query/src/types.ts 中的定义紧邻且对称:
// L364-L367:无 initialData → 数据可能为空 export type UseInfiniteQueryResult<TData = unknown, TError = DefaultError> = InfiniteQueryObserverResult<TData, TError> // L376-L379:有 initialData → 数据必定有值 export type DefinedUseInfiniteQueryResult<TData = unknown, TError = DefaultError> = DefinedInfiniteQueryObserverResult<TData, TError>useInfiniteQuery正是靠函数重载在这两者间自动切换(实现见 packages/preact-query/src/useInfiniteQuery.ts):
- 第 64 行的重载接收
DefinedInitialDataInfiniteOptions,返回DefinedUseInfiniteQueryResult<TData, TError>,条件是initialData已设置; - 第 189 行与第 344 行的重载分别接收
UndefinedInitialDataInfiniteOptions与通用UseInfiniteQueryOptions,返回UseInfiniteQueryResult<TData, TError>(详见 useInfiniteQuery 官方函数文档)。
从调用方看,你只需要写一次useInfiniteQuery({...}),TypeScript 会依据选项对象里是否存在initialData自动选出正确的重载与结果类型,无需任何手动注解。
无 initialData 时的类型守卫示例
import { useInfiniteQuery } from '@tanstack/preact-query' function Projects() { const { data, isPending, isError, error, fetchNextPage, hasNextPage, isFetching } = useInfiniteQuery({ queryKey: ['projects'], queryFn: ({ pageParam }) => fetchProjects(pageParam), initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextId, }) // 必须先做守卫,data 才会从 undefined 被收窄为有值类型 if (isPending) return 'Loading...' if (isError) return <span>Error: {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> </> ) }与之相对,配置了initialData的用法(返回DefinedUseInfiniteQueryResult)可直接解构data.pages渲染,示例见 useInfiniteQuery 函数文档中的 initialData 用例。
五、与 Query / Suspense 相关结果类型的边界
该类型属于 Preact 结果类型族,理解它与兄弟类型的关系有助于避免误用:
| 类型别名 | 适用 Hook | data是否可为undefined | 定义位置 |
|---|---|---|---|
UseQueryResult | useQuery(无 initialData) | 可为undefined | types.ts#L324-L327 |
UseInfiniteQueryResult | useInfiniteQuery(无 initialData) | 可为undefined | types.ts#L364-L367 |
DefinedUseInfiniteQueryResult | useInfiniteQuery(有 initialData) | 恒有值 | types.ts#L376-L379 |
UseSuspenseInfiniteQueryResult | useSuspenseInfiniteQuery | 恒有值(Suspense 语义,data必达) | types.ts#L388-L394 |
其中UseSuspenseInfiniteQueryResult相当于在DefinedUseInfiniteQueryResult基础上移除了isPlaceholderData字段(Suspense 模式下永不渲染占位数据),可对照阅读 DefinedUseInfiniteQueryResult 与 UseSuspenseInfiniteQueryResult 的文档。
若你想在共享选项时预先把类型固定下来(而不依赖 Hook 调用点的推导),官方推荐使用 infiniteQueryOptions 帮助函数,它返回 UseInfiniteQueryOptions 结构,可同时传给useInfiniteQuery与命令式 APIqueryClient.infiniteQuery。
六、无限查询结果的特有字段:超越普通 useQuery
UseInfiniteQueryResult展开后,除继承QueryObserverBaseResult(data、error、status、isPending、isFetching、refetch等)外,还通过InfiniteQueryObserverBaseResult追加了无限分页专属成员(见 packages/query-core/src/types.ts#L904-L944):
| 字段 | 类型 | 含义 |
|---|---|---|
fetchNextPage | (options?) => Promise<InfiniteQueryObserverResult<TData, TError>> | 拉取下一页(即调用端"加载更多") |
fetchPreviousPage | (options?) => Promise<InfiniteQueryObserverResult<TData, TError>> | 拉取上一页 |
hasNextPage | boolean | 依据getNextPageParam判断是否还有下一页 |
hasPreviousPage | boolean | 依据getPreviousPageParam判断是否还有上一页 |
isFetchingNextPage | boolean | 是否正在拉取下一页 |
isFetchingPreviousPage | boolean | 是否正在拉取上一页 |
isFetchNextPageError | boolean | 拉取下一页失败(首次加载失败不算) |
isFetchPreviousPageError | boolean | 拉取上一页失败 |
而data内部则是InfiniteData形态(types.ts#L210-L213):
export interface InfiniteData<TData, TPageParam = unknown> { pages: Array<TData> // 每页数据,按页序累积 pageParams: Array<TPageParam> // 与 pages 一一对应的页参数(cursor) }渲染时通常用data.pages.map(page => ...)平铺所有已加载页;页参数data.pageParams主要供调试或二次请求时使用。注意这 8 个无限查询字段即使处于pending/error状态也始终存在(它们定义于所有分支共同继承的BaseResult上),因此可在加载态之外安全调用fetchNextPage等命令式函数。
七、结合 useBaseQuery:返回值在运行时如何产生
从 packages/preact-query/src/useInfiniteQuery.ts#L361-L369 的实现看,useInfiniteQuery并未自建状态机,而是把InfiniteQueryObserver作为 observer 类型传入内部公共实现useBaseQuery:
export function useInfiniteQuery( options: UseInfiniteQueryOptions, queryClient?: QueryClient, ) { return useBaseQuery( options, InfiniteQueryObserver as typeof QueryObserver, queryClient, ) }运行时的结果对象(即运行态的UseInfiniteQueryResult)由InfiniteQueryObserver在@tanstack/query-core内部计算并发布,Preact 层只负责订阅、跟踪(tracking)与触发重渲染。这正是「类型在 preact-query 中重导出、行为在 query-core 中实现」的分层体现,也让fetchNextPage的返回值类型与结果对象类型在引用上形成闭环。
需要特别留意官方文档的提醒:命令式获取(如fetchNextPage)可能干扰默认的自动 refetch 行为,导致数据过期。因此应仅在响应用户操作时调用这些函数,或加上hasNextPage && !isFetching之类的守卫(参考 useInfiniteQuery 函数文档的 Remarks 段落)。如果你想在首次加载后并发提前预取后续页,应优先考虑usePrefetchInfiniteQuery(函数文档)。
八、实战模式:滚动到底部自动加载、禁用态与可选链
结合类型语义,下面三种是 useInfiniteQuery 函数文档 中给出的典型消费模式:
1. IntersectionObserver 无限滚动
用哨兵元素(sentinel)观察是否进入视口,配合hasNextPage && !isFetching守卫触发fetchNextPage:
import { useInfiniteQuery } from '@tanstack/preact-query' import { useEffect, useRef } from 'preact/hooks' function Projects() { const { data, isPending, isError, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } = useInfiniteQuery({ queryKey: ['projects'], queryFn: ({ pageParam }) => fetchProjects(pageParam), initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextId, }) const sentinelRef = useRef<HTMLDivElement>(null) useEffect(() => { const sentinel = sentinelRef.current if (sentinel == null || !hasNextPage || isFetching) return const observer = new IntersectionObserver(([entry]) => { if (entry?.isIntersecting) fetchNextPage() }) observer.observe(sentinel) return () => observer.disconnect() }, [hasNextPage, isFetching, fetchNextPage]) if (isPending) return 'Loading...' if (isError) return <span>Error: {error.message}</span> return ( <> <ul> {data.pages.map((page) => page.projects.map((project) => <li key={project.id}>{project.name}</li>), )} </ul> <div ref={sentinelRef}>{isFetchingNextPage ? 'Loading more...' : null}</div> </> ) }2. 用 skipToken 代替 enabled: false 表达禁用态
当依赖参数尚未就绪时,将skipToken作为queryFn,查询会被安全地跳过。此时由于返回类型仍属于UseInfiniteQueryResult,需要借助isLoading(而非isPending)区分「禁用中」与「真正加载中」,并且data需走可选链:
import { skipToken, useInfiniteQuery } from '@tanstack/preact-query' function Comments({ postId }: { postId: string | undefined }) { const { data, isLoading, isError, error } = useInfiniteQuery({ queryKey: ['post', postId, 'comments'], queryFn: postId != null ? ({ pageParam }) => fetchComments(postId, pageParam) : skipToken, initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextId, }) if (postId == null) return 'Select a post' if (isLoading) return 'Loading...' if (isError) return <span>Error: {error.message}</span> return ( <ul> {data?.pages.map((page) => page.comments.map((c) => <li key={c.id}>{c.text}</li>))} </ul> ) }3. 何时不需要守卫:initialData / Suspense / 预取
当使用initialData(返回DefinedUseInfiniteQueryResult)、useSuspenseInfiniteQuery,或通过queryClient.ensureInfiniteQueryData等预取 API 后,类型上data恒有值,组件可以放心直接访问data.pages而不必先做isPending分支——类型系统已经从源头排除了undefined。
更多面向 Preact 的分页配置(initialPageParam、getNextPageParam、getPreviousPageParam、maxPages)与 fetch 行为说明,可继续阅读指南 infinite-queries.md 以及 UseInfiniteQueryOptions 接口文档;若想复用同一份选项到命令式 API,参见 infiniteQueryOptions。
小结
UseInfiniteQueryResult<TData, TError>本质是InfiniteQueryObserverResult在 Preact 适配层的重导出:它通过五分支可辨识联合,把「无initialData时data在 pending 阶段为undefined」这一运行时事实建模进了类型系统。使用时牢记两点即可:其一,访问data.pages/data.pageParams前先做isPending/isError守卫;其二,配置了initialData时类型会自动切换为DefinedUseInfiniteQueryResult,从而获得无守卫的data访问体验。
【免费下载链接】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),仅供参考