首页
/ Preact Query 的 infiniteQueryOptions:以强类型配置工厂串联 useInfiniteQuery 与命令式 API

Preact Query 的 infiniteQueryOptions:以强类型配置工厂串联 useInfiniteQuery 与命令式 API

2026-09-08 09:54:53作者:齐添朝

导读

在 TanStack Query(本仓库中的 Preact Query 实现,包名 @tanstack/preact-query)中,无限滚动/分页加载通常由 useInfiniteQuery 完成;而 infiniteQueryOptions 则是一个类型安全的配置工厂函数,把「一份可用于无限查询的 options 对象」从组件内部抽离出来,使其能在 useInfiniteQueryuseSuspenseInfiniteQuery、预取辅助以及 queryClient.infiniteQuery 等命令式 API 之间自由复用。读完本文,你将掌握 infiniteQueryOptions 的三个函数重载各自适用于什么场景、initialDataskipToken 如何影响返回的数据类型,以及如何用参数化工厂模式为不同业务键复用同一份查询配置,并理解其背后的类型标记(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 理解为它在无限查询领域的孪生版本。区别在于:

  • 普通查询的数据形态是单页数据;无限查询的数据被包成 InfiniteData<TData, TPageParam>(见下方说明);
  • 无限查询额外要求 initialPageParamgetNextPageParam 等分页相关配置;
  • 无限查询的配置类型参数多出一个 TPageParam,用于描述每次翻页传给 queryFn 的参数。

三个重载与类型参数全解

infiniteQueryOptions 在类型层面定义了三个重载,分别应对「是否设置了 initialData」「queryFn 是否允许为 skipToken」等不同形态,从而在编译期为每种用法推导出最精确的返回类型。文档中给出的签名如下:

function infiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>(options): UseInfiniteQueryOptions<...> & object & QueryKeyWithDataTag<TQueryKey, InfiniteData<TQueryFnData, unknown>, TError>
重载位置 触发条件 options 参数类型 关键约束
第一个 设置了 initialData DefinedInitialDataInfiniteOptions initialData 必填,data 永不为 undefined
第二个 未设置 initialDataqueryFn 不能是 skipToken UnusedSkipTokenInfiniteOptions queryFn 排除了 SkipToken
第三个 未设置 initialData(最通用) UndefinedInitialDataInfiniteOptions queryFn 可为普通函数,数据可能处于 pending

三个重载的实现位置分别在 infiniteQueryOptions.ts 的第 171、233、295 行附近,对应的 options 类型别名文档可分别参阅 DefinedInitialDataInfiniteOptionsUnusedSkipTokenInfiniteOptionsUndefinedInitialDataInfiniteOptions

五个泛型参数逐一说明

类型参数 默认值 含义
TQueryFnData 无(必推) 单页数据的类型,即你的 queryFn 解析出的结果类型
TError DefaultError(默认即 Error queryFn 可能抛出的错误类型
TData InfiniteData<TQueryFnData, unknown> select 处理后 data 的最终类型;默认是「所有已抓取页面 + 页码参数」的聚合形态
TQueryKey readonly unknown[](即 QueryKey 查询键的类型
TPageParam unknown 传给 queryFn 用于抓取某一页的页码参数类型

其中 TData 的默认形态 InfiniteDatapackages/query-core/src/types.ts 中定义得非常直观:

export interface InfiniteData<TData, TPageParam = unknown> {
  pages: Array<TData>
  pageParams: Array<TPageParam>
}

也就是说,无限查询的 data 永远由「页面内容数组 pages」和「每次翻页用到的参数数组 pageParams」两部分组成,pageParams 中记录了每一页分别是用哪个参数抓回来的,二者按序一一对应。

返回值的类型标记

无论命中哪个重载,函数都返回 同一个 options 对象,只是返回类型中叠加了 QueryKeyWithDataTag<TQueryKey, InfiniteData<TQueryFnData, unknown>, TError>,让 queryKey 携带数据与错误类型标记。QueryKeyWithDataTagquery-core/src/types.ts 中的定义是:

export type QueryKeyWithDataTag<
  TQueryKey extends QueryKey = QueryKey,
  TQueryFnData = unknown,
  TError = DefaultError,
> = {
  queryKey: DataTag<TQueryKey, TQueryFnData, TError>
}

DataTag 通过特殊符号把「该查询键对应什么数据、什么错误」烙在 queryKey 的类型上。这样一来,当同一份 options 被传入 queryClient 的缓存读取类方法时,编译器也能从 query key 直接反推出 dataerror 的类型,而不需要再手动补泛型——这正是「配置定义一次、处处类型安全」的核心机制。

快速上手:把配置抽到组件外

官方文档提供的第一个示例,展示了最基本的用法:把无限查询配置定义成模块级的 projectsOptions,再原样交给 useInfiniteQuery

import { 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 ? <span>Error: {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.pagesdata.pageParams 外,还包含 fetchNextPagefetchPreviousPagehasNextPagehasPreviousPageisFetchingNextPageisFetchingPreviousPage 等。

设置 initialData 的重载:让 data 永不为空

第一个重载要求 initialData 存在。它对应的 options 类型是 DefinedInitialDataInfiniteOptions,其中的 initialData 有三种合法写法(见 infiniteQueryOptions.ts):

initialData:
  | NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>
  | (() => NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>)
  | undefined

源码注释(同样保留在 DefinedInitialDataInfiniteOptions 文档中)对其语义作了精确说明:

  • initialData 会被用作查询缓存的初始数据——前提是该查询尚未被创建或尚未有缓存;
  • 如果传入的是函数,函数只会在共享/根查询初始化期间被调用一次,且必须同步返回初始数据;
  • initialData 默认被视为过期数据(stale),除非设置了 staleTime
  • initialData 会持久化写入缓存

也就是说,它不是组件挂载后为了“好看”临时塞进渲染层的占位符,而是会真实参与缓存与过期判断的真实数据。设置 initialData 后,类型层面即可保证 data 不会是 undefined,这也是它与「无 initialData 重载」最本质的类型差异。

未设置 initialData 的重载与 skipToken 的边界

如果你没有设置 initialData,编译器会在剩余两个重载中继续区分 queryFn 是否允许是 skipToken。最通用的第三个重载(UndefinedInitialDataInfiniteOptions)对应标准形态:查询处于 pendingdata 可能是 undefined,因此组件里通常要先用 isPending 做加载判断。

值得注意的是第二个重载 UnusedSkipTokenInfiniteOptions:它排除了 skipToken 作为 queryFn 的可能,并借助 OmitKeyof<..., 'queryFn'>queryFn 字段做了收紧。为什么这样设计?类型定义中给出的原话值得逐句细读(见 infiniteQueryOptions.ts):

skipToken is not allowed as a value here — this overload is selected when no initialData is set. If you don't intend to run the query yet, set enabled: false — omitting queryFn alone still triggers a fetch that fails with "Missing queryFn" unless enabled is false or a default query function has been defined.

翻译成实践结论:

  • 无限查询语境下,skipTokeninitialData 不应同时使用,二者走的是不同的重载分支;
  • 如果暂时不想发起请求,正确做法是设置 enabled: false,而不是省略 queryFn——省略 queryFn 仍会触发抓取并因 “Missing queryFn” 失败(除非 enabledfalse);
  • 即便定义了默认查询函数(default query function),它也只会补上 queryFn,本身不会推迟请求的发起。

参数化工厂模式:一份工厂,多个业务键复用

对于评论列表这类「每个业务实体(如 postId)都有一套独立无限查询」的场景,文档给出了参数化工厂的推荐写法。它实际上返回一个函数,每次传入不同的 postId 就生成一份带独立 queryKey 的 options:

import { 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 <span>Error: {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 复用:预取与 Suspense

infiniteQueryOptions 文档反复强调:同一份 options 可以被 Hook 与命令式 API 共享。仓库中 preact-query 提供了现成的消费方,全部接受该工厂生成的配置对象:

1. usePrefetchInfiniteQuery——见 packages/preact-query/src/usePrefetchInfiniteQuery.tsxusePrefetchInfiniteQuery 文档。它专门用来在 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 生成的 projectsOptions

import { 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={<h1>Loading projects...</h1>}>
      <Projects />
    </Suspense>
  )
}

注意预取场景的约束:queryKeyinitialPageParamgetNextPageParam 始终必填,queryFn 在未定义默认查询函数时也必填,且 queryFn 不允许为 skipToken(见 types.tsUsePrefetchInfiniteQueryOptions 的定义)。

2. useSuspenseInfiniteQuery——Suspense 形态的无限查询 Hook,文档见 docs/framework/preact/reference/functions/useSuspenseInfiniteQuery.md。它的 options 类型 UseSuspenseInfiniteQueryOptionsUseInfiniteQueryOptions 几乎一致,只是剔除了 enabledthrowOnErrorplaceholderData(Suspense Hook 无法渲染“禁用”或“占位”状态),且同样不允许 queryFnskipToken(见 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 GetNextPageParamFunction<TPageParam, TQueryFnData = unknown> = (
  lastPage: TQueryFnData,
  allPages: Array<TQueryFnData>,
  lastPageParam: TPageParam,
  allPageParams: Array<TPageParam>,
) => TPageParam | undefined | null

它最多接收四个实参:最后一页数据、全部页面数据、最后一个页码参数、全部页码参数;返回值若为 undefinednull,则代表没有下一页。InfiniteData.pages.pageParams 正是由这类函数逐页驱动填充的。

packages/query-core/src/infiniteQueryBehavior.ts 负责把这些声明转换为实际抓取行为,其中:

  • 首屏会使用 initialPageParam(在无已有页码参数时兜底取 oldPageParams[0] ?? options.initialPageParam);
  • 抓取单页时会把 pageParam 写入 queryFn 的执行上下文(context.fetchFn);
  • getNextPageParamhasNextPage 协作决定是否还有下一页。

这些行为被大量测试覆盖,例如 packages/query-core/src/tests/infiniteQueryObserver.test.tsx 中验证了:getNextPageParam 返回 undefinednull 时停止继续抓取下一页、initialPageParamnull 时也能正常抓取首页、以及页面为空时不调用 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 的价值在于复用,而非强制。

实现层面,它的运行时仅是恒等返回、全部智能由类型系统承载,因此把它当作纯编译期工具来理解即可:一份配置、处处复用、处处类型安全

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.79 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
390