news 2026/9/10 8:39:11

TanStack Solid Query 的 useInfiniteQuery:无限滚动与分页加载的完整实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TanStack Solid Query 的 useInfiniteQuery:无限滚动与分页加载的完整实战指南

TanStack Solid Query 的 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

导读

useInfiniteQuery是 TanStack Solid Query(@tanstack/solid-query)提供的无限查询 Hook,专门用于"加载更多"(分页、游标、无限滚动)场景,例如 Feed 流、聊天记录、商品列表等需要逐页追加数据的界面。它基于useQuery的全部能力构建,额外引入了页码参数(pageParam)、双向翻页(下一页/上一页)与页数上限(maxPages)机制。读完本文,你将掌握useInfiniteQuery的完整 API、全部配置项与返回属性、底层实现原理,以及如何在 SolidJS 组件中实现一个可靠的无限滚动列表。

本文以 useInfiniteQuery.md 为骨架,结合 solid-query 源码 与 query-core 的无限查询实现 进行纵深讲解,确保每个结论都有文档与源码双重依据。

一、为什么需要 useInfiniteQuery:与 useQuery 的本质差异

普通useQuery一次只获取一份数据;而无限查询需要面对三个useQuery无法优雅处理的问题:

  1. 分页参数需要记忆与累积:每次加载新页时,需要用上一次返回的游标/页码作为下一次请求的参数,并且所有已加载的页要拼接成一个列表;
  2. "是否还有下一页/上一页"需要推导:该状态由getNextPageParam/getPreviousPageParam的返回值决定,是触发"加载更多"按钮或 Intersection Observer 的依据;
  3. 页数需要受控:数据无限增长会拖垮内存与渲染,maxPages用于裁剪过旧/过新的页。

从源码看,useInfiniteQuery在 Solid 层的实现非常薄——它本质上是把InfiniteQueryObserver交给共享的useBaseQuery基座来处理:

// packages/solid-query/src/useInfiniteQuery.ts export function useInfiniteQuery<TQueryFnData, TError, TData, TQueryKey, TPageParam>( options: UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>, queryClient?: Accessor<QueryClient>, ): UseInfiniteQueryResult<TData, TError> { return useBaseQuery( createMemo(() => options()), InfiniteQueryObserver as typeof QueryObserver, queryClient, ) as UseInfiniteQueryResult<TData, TError> }

可以看到:options以 Accessor(函数)形式传入并被createMemo包裹,这意味着它可以响应式地跟踪 SolidJS 信号的变化(与 useQuery 的"Reactive Options"设计一致);而真正的"无限"逻辑全部封装在InfiniteQueryObserverinfiniteQueryBehavior中(见下文"底层原理")。

二、API 签名总览

useInfiniteQuery的完整调用形式如下(来自 useInfiniteQuery.md 的 API 示例):

const { fetchNextPage, fetchPreviousPage, hasNextPage, hasPreviousPage, isFetchingNextPage, isFetchingPreviousPage, ...result } = useInfiniteQuery(() => ({ queryKey, queryFn: ({ pageParam }) => fetchPage(pageParam), initialPageParam: 1, ...options, getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => lastPage.nextCursor, getPreviousPageParam: (firstPage, allPages, firstPageParam, allPageParams) => firstPage.prevCursor, }))

函数签名与useQuery一致,都是两个参数:

  • 第一个参数Accessor<InfiniteQueryOptions>——返回配置对象的函数,用于响应式跟踪(见 useInfiniteQuery.ts);
  • 第二个参数(可选)Accessor<QueryClient>——自定义 QueryClient,缺省时使用最近上下文(QueryClientProvider)中的实例。

类型层面,UseInfiniteQueryResult就是 query-core 的InfiniteQueryObserverResult,其数据类型固定为InfiniteData<TData, TPageParam>,结构为{ pages: TData[]; pageParams: TPageParam[] }(见 solid-query/src/types.ts)。

提示:源码中还提供了infiniteQueryOptions辅助函数(见 infiniteQueryOptions.ts),它本身是恒等函数(原样返回 options),主要作用是利用 TypeScript 类型推断把data属性标注为InfiniteData,在把配置对象抽离到组件外时获得更精确的类型推导。

三、Options:无限查询专属配置项

useInfiniteQuery的选项与 useQuery hook 完全一致,额外增加以下 5 个配置项。

3.1 queryFn —— 请求函数(接收 pageParam)

  • 必填(除非已通过 QueryClient 定义了默认查询函数defaultQueryFn)。
  • 查询用来请求数据的函数,接收一个 QueryFunctionContext。
  • 必须返回一个 Promise:resolve 出数据或 throw 出错误。数据不能是undefined

在无限查询中,QueryFunctionContext上多出了两个关键字段(由 infiniteQueryBehavior.ts 构造):

const queryFnContext = { client, queryKey, pageParam: param, // 本次要获取的页码/游标 direction: previous ? 'backward' : 'forward', // 获取方向 meta, }

因此queryFn: ({ pageParam }) => fetchPage(pageParam)中解构出的pageParam正是由getNextPageParam/getPreviousPageParam计算出来并注入的。

3.2 initialPageParam —— 首页参数

  • 必填
  • 获取第一页时使用的默认 pageParam。

在 infiniteQueryBehavior.ts 中,首次拉取(没有 direction、没有旧页)时:

const param = currentPage === 0 ? (oldPageParams[0] ?? options.initialPageParam) : getNextPageParam(options, result)

即:第一页优先使用已缓存的第一页参数(如initialData带来的),否则使用initialPageParam;后续每一页用getNextPageParam推导。

3.3 getNextPageParam —— 推导下一页参数

  • 必填
  • 签名:(lastPage, allPages, lastPageParam, allPageParams) => TPageParam | undefined | null
  • 当查询收到新数据时,此函数接收无限列表的最后一页全部页数组,以及对应的 pageParam 信息。
  • 它应返回单个变量,该变量会作为pageParam传入 queryFn 的 context(如queryFn: ({ pageParam }) => ...)。
  • 返回undefinednull表示没有下一页。

典型用法(游标分页):

getNextPageParam: (lastPage) => lastPage.nextCursor,

底层实现中,该函数由getNextPageParam辅助函数调用,且仅在"已有页"时才会被调用(见 infiniteQueryBehavior.ts):

function getNextPageParam(options, { pages, pageParams }) { const lastIndex = pages.length - 1 return pages.length > 0 ? options.getNextPageParam(pages[lastIndex], pages, pageParams[lastIndex], pageParams) : undefined }

注意hasNextPage正是"getNextPageParam返回值不为null/undefined"的直接映射(见 infiniteQueryBehavior.ts),所以只要它返回了有效值,hasNextPage即为true

3.4 getPreviousPageParam —— 推导上一页参数

  • 可选(与getNextPageParam不同,文档未标记为 Required;从源码getPreviousPageParam?.的可选调用也能印证)。
  • 签名:(firstPage, allPages, firstPageParam, allPageParams) => TPageParam | undefined | null
  • 当查询收到新数据时,此函数接收无限列表的第一页全部页数组及 pageParam 信息。
  • 返回的单个变量将作为pageParam传入 queryFn 的 context。
  • 返回undefinednull表示没有上一页。

它服务于"向上翻页"(如聊天记录往上加载)场景。hasPreviousPage在未定义该函数或返回空值时均为false(见 infiniteQueryBehavior.ts)。

3.5 maxPages —— 页数上限

  • 类型:number | undefined,默认undefined
  • 无限查询数据中最多存储的页数。
  • 当达到最大页数后,再获取新页会导致pages数组头部或尾部被移除,具体取决于获取方向:
    • 向后加载(fetchNextPage)时移除第一页addToEnd,见 infiniteQueryBehavior.ts);
    • 向前加载(fetchPreviousPage)时移除最后一页addToStart)。
  • 若为undefined0,页数不受限制。
  • maxPages > 0,必须正确定义getNextPageParamgetPreviousPageParam,以便在需要时两个方向都能拉取页面。

页的追加/裁剪由utils中的addToEnd/addToStart完成(对pagespageParams同步操作,保证两者长度一致)。

使用建议:对"只增不减"的 Feed 流可保持undefined;对内存敏感或需要固定窗口(如仅保留最近 N 页)的场景设置具体数值。要兼顾"向上加载"与"向下加载",同时启用maxPages时务必把两个 pageParam 函数都配齐。

四、Returns:返回属性详解

useInfiniteQuery返回一个 SolidJS Store(Store<InfiniteQueryObserverResult>),其属性与 useQuery hook 一致(含statusisPendingisSuccessisErrordataUpdatedAterrorfailureCountfetchStatusrefetch等),差异点在于:

  1. data不是普通数据,而是{ pages, pageParams }结构(见 4.1);
  2. 新增fetchNextPage/fetchPreviousPage/hasNextPage/hasPreviousPage等 6 个方向性属性;
  3. isRefetchingisRefetchError的语义有细微差别(见 4.8、4.9)。

这些新属性的计算集中在一个地方——InfiniteQueryObserver.createResult(见 infiniteQueryObserver.ts),它以state.fetchMeta?.fetchMore?.direction'forward'/'backward')区分当前请求的方向。

4.1 data.pages 与 data.pageParams

  • data.pages: TData[]——包含所有已加载页的数组。
  • data.pageParams: unknown[]——与pages一一对应的 pageParam 数组(第 i 个 page 是用第 i 个 pageParam 请求来的)。

两个数组在每次拉页时同步更新(见 3.5 中的addTo逻辑),因此用data.pages渲染列表、用data.pageParams做去重或调试都很方便。

注意:data本身是 SolidJS Resource(见 useQuery.md 对data: Resource<TData>的说明)。如果data<Suspense>下被访问且数据尚不可用,会触发 Suspense 边界;这在 SSR/流式渲染场景中要留意。

4.2 fetchNextPage / fetchPreviousPage

  • fetchNextPage: (options?: FetchNextPageOptions) => Promise<UseInfiniteQueryResult>——获取"下一页"。
  • fetchPreviousPage: (options?: FetchPreviousPageOptions) => Promise<UseInfiniteQueryResult>——获取"上一页"。
  • options.cancelRefetch: boolean
    • true(默认)时,重复调用fetchNextPage每次都触发queryFn,即使上一次调用尚未 resolve;且前一次调用的结果会被忽略。
    • false时,在第一次调用 resolve 之前,重复调用不产生任何效果。

从源码看,这两个方法只是给内部fetch挂上方向标记(见 infiniteQueryObserver.ts):

fetchNextPage(options?: FetchNextPageOptions) { return this.fetch({ ...options, meta: { fetchMore: { direction: 'forward' } } }) } fetchPreviousPage(options?: FetchPreviousPageOptions) { return this.fetch({ ...options, meta: { fetchMore: { direction: 'backward' } } }) }

infiniteQueryBehavior根据该direction决定使用getNextPageParam还是getPreviousPageParam计算本次要请求的参数,以及新页追加到头部还是尾部(见 infiniteQueryBehavior.ts)。

4.3 hasNextPage / hasPreviousPage

  • hasNextPage: boolean——通过getNextPageParam判断:若其返回值不为null/undefined则为true
  • hasPreviousPage: boolean——通过getPreviousPageParam判断,且当该函数未定义时恒为false

实现见 infiniteQueryBehavior.ts。它们是"加载更多"按钮的天然开关,常用写法:

<Show when={state.hasNextPage && !state.isFetchingNextPage}> <button onClick={() => state.fetchNextPage()}>加载更多</button> </Show>

4.4 isFetchingNextPage / isFetchingPreviousPage

  • 当通过fetchNextPage获取下一页的过程中为true
  • 当通过fetchPreviousPage获取上一页的过程中为true

推导逻辑(见 infiniteQueryObserver.ts):

const isFetchingNextPage = isFetching && fetchDirection === 'forward' const isFetchingPreviousPage = isFetching && fetchDirection === 'backward'

它们通常用来显示"底部加载中"的占位提示,或阻止重复点击。

4.5 isFetchNextPageError / isFetchPreviousPageError

  • isFetchNextPageError: boolean——获取下一页失败时为true
  • isFetchPreviousPageError: boolean——获取上一页失败时为true

同样由方向判定(infiniteQueryObserver.ts):

const isFetchNextPageError = isError && fetchDirection === 'forward' const isFetchPreviousPageError = isError && fetchDirection === 'backward'

4.6 isRefetching(无限查询专属语义)

  • 只要**后台重取(background refetch)**进行中即为true不包含初始pending、也不包含下一页/上一页的获取。
  • 等价于:isFetching && !isPending && !isFetchingNextPage && !isFetchingPreviousPage

源码中的实现(infiniteQueryObserver.ts):

isRefetching: isRefetching && !isFetchingNextPage && !isFetchingPreviousPage,

对比 useQuery 的isRefetchingisFetching && !isPending),无限查询把方向性获取排除在外,避免加载更多时误触发"正在刷新"的 UI 状态。

4.7 isRefetchError(无限查询专属语义)

  • 查询在重取页面(非首次、非方向性获取)时失败为true
  • 源码中排除了方向性获取错误(infiniteQueryObserver.ts):
isRefetchError: isRefetchError && !isFetchNextPageError && !isFetchPreviousPageError,

五、完整的实战示例

下面是一个基于 SolidJS +@tanstack/solid-query的游标分页列表示例,覆盖了响应式配置、加载更多按钮与完整的状态分支。它综合了 useInfiniteQuery.md 的 API 示例、useQuery.md 的组件写法(Show/For/createSignal),并参考了 useInfiniteQuery.test.tsx 中的调用方式(如fetchNextPage由按钮点击触发)。

import { createSignal, Show, For } from 'solid-js' import { useInfiniteQuery } from '@tanstack/solid-query' interface Page { items: Array<{ id: number; name: string }> nextCursor: number | null } function fetchPage(pageParam: number): Promise<Page> { return fetch(`/api/items?cursor=${pageParam}`).then((res) => { if (!res.ok) throw new Error('Failed to fetch') return res.json() }) } function ItemList() { const [enabled, setEnabled] = createSignal(true) const state = useInfiniteQuery(() => ({ queryKey: ['items'], enabled: enabled(), // 响应式配置:queryKey / queryFn 内部都可以读取信号 queryFn: ({ pageParam }) => fetchPage(pageParam), initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextCursor, // 返回 null 即没有下一页 })) return ( <div> <Show when={state.isLoading}> <div>首次加载中…</div> </Show> <Show when={state.isError}> <div>加载失败: {state.error?.message}</div> </Show> <Show when={state.isSuccess}> <ul> <For each={state.data?.pages.flatMap((p) => p.items)}> {(item) => <li>{item.name}</li>} </For> </ul> {/* 加载更多按钮:hasNextPage 控制显隐,isFetchingNextPage 防重复点击 */} <Show when={state.hasNextPage && !state.isFetchingNextPage}> <button onClick={() => state.fetchNextPage()}>加载更多</button> </Show> <Show when={state.isFetchingNextPage}> <div>加载下一页中…</div> </Show> <Show when={state.isFetchNextPageError}> <div>下一页加载失败</div> </Show> </Show> </div> ) }

5.1 实现要点

  • 响应式选项useInfiniteQuery(() => ({ ... }))的函数体在 SolidJS 响应式作用域内执行,enabled()filter()等信号变化会自动触发查询重建(与 useQuery 的 Reactive Options 一致)。测试中也验证了选项变化后查询能正确重新执行。
  • 游标为null表示到底getNextPageParam返回nullhasNextPage变为false,按钮自动隐藏(测试用例'should set hasNextPage to false if getNextPageParam returns undefined'验证了该行为,见 useInfiniteQuery.test.tsx)。
  • 错误分支isFetchNextPageError专门标识"加载更多"失败,可用于在按钮位置展示重试入口;测试用例'should return the correct states when fetchNextPage fails'对此有完整覆盖。
  • cancelRefetch 的取舍:默认true下快速连点会重复请求并忽略旧结果;若想"一次加载中禁止再次请求",传{ cancelRefetch: false }——测试'should not cancel an ongoing fetchNextPage request when another fetchNextPage is invoked if cancelRefetch: false is used'验证了该语义。

5.2 与 Suspense / ErrorBoundary 结合

由于data是 SolidJS Resource,可配合SuspenseErrorBoundary声明式处理首屏加载与错误(与 useQuery 的 Suspense 用法 相同,注意设置throwOnError: true):

<ErrorBoundary fallback={(err) => <div>出错了: {err.message}</div>}> <Suspense fallback={<div>加载中…</div>}> <ItemList /> </Suspense> </ErrorBoundary>

六、底层原理:一次 fetchNextPage 调用发生了什么

把上面的 API 串起来,一次fetchNextPage()的完整链路是:

  1. Solid 层useInfiniteQueryoptions(Accessor)交给useBaseQuery(见 useBaseQuery.ts),后者创建InfiniteQueryObserver实例并用createResource包装观察结果,返回带 Proxy 的 Store(对data做了 Resource 访问转发,见 useBaseQuery.ts)。
  2. Observer 层fetchNextPage()meta.fetchMore.direction = 'forward'调用fetch(infiniteQueryObserver.ts)。
  3. Behavior 层infiniteQueryBehavioronFetch读取direction与旧数据:
    • 有旧页且带方向 → 用getNextPageParam(options, oldData)算出本次param,然后fetchPage(oldData, param)把新页追加到尾部(addToEnd),并受maxPages裁剪;
    • 无方向(首次/重取)→ 从initialPageParam开始循环拉页直到达到remainingPages(infiniteQueryBehavior.ts)。
  4. 结果合并createResult基于fetchDirection推导出isFetchingNextPageisFetchNextPageError等 6 个方向性属性,并修正isRefetching/isRefetchError的语义(infiniteQueryObserver.ts)。
  5. 响应式传播:Store 更新后,组件中读取state.datastate.isFetchingNextPage的地方自动重渲染。

这套设计的好处:方向状态不放在 Solid 层,而是内聚在 query-core 的 fetchMeta 中,因此useInfiniteQueryuseQuery的响应式基座完全共享,行为差异全部由InfiniteQueryObserversetOptions时标记_type = 'infinite',见 infiniteQueryObserver.ts)与 Behavior 承担,Solid 包装层保持极薄。

七、注意事项与最佳实践

  • 命令式翻页可能干扰默认重取fetchNextPage这类命令式调用会与默认的重取行为(如refetchOnWindowFocusrefetchInterval)相互干扰,可能导致展示过期数据。请仅在用户动作的响应中调用(点击、滚动触底回调),或加上hasNextPage && !isFetching之类的条件再调用。
  • hasNextPage 依赖服务端返回getNextPageParam的返回值直接决定hasNextPage,因此服务端必须在每页响应中携带可判空的游标字段(如nextCursor: number | null)。
  • maxPages 与双向加载:启用maxPages且需要"向上/向下都能翻"时,务必同时定义getNextPageParamgetPreviousPageParam,否则裁剪方向会失去对侧可拉取的参数来源。
  • 不要在 render 中直接调用 fetch 方法fetchNextPage应挂在事件回调(onClick)或createEffect中,避免渲染期间触发副作用。
  • 服务端渲染注意:SSR 期间useBaseQuery会把retry置为falsethrowOnError置为true(见 useBaseQuery.ts),且fetchNextPage/fetchPreviousPage等函数在序列化时会被置空、水合后恢复(见 useBaseQuery.ts),因此 SSR 输出中不要依赖这些函数。
  • 类型提示:若把配置对象抽到组件外,可借助infiniteQueryOptions包装以获得dataInfiniteData类型推导(见 infiniteQueryOptions.ts)。

八、扩展阅读

  • useQuery hook:useInfiniteQuery的选项与返回值以它为基底,包含enabledselectstaleTimegcTimerefetchInterval等全部通用配置的详细说明。
  • Query Keys:queryKey的哈希与自动更新机制。
  • Query Functions:QueryFunctionContext中各字段(含无限查询注入的pageParamdirection)的完整说明。
  • Default Query Function:省略queryFn时的默认函数定义方式。
  • Network Mode:离线/弱网下的取数行为与fetchStatus语义。
  • 核心实现:useInfiniteQuery.ts、infiniteQueryObserver.ts、infiniteQueryBehavior.ts。
  • 测试用例:useInfiniteQuery.test.tsx(覆盖加载更多、失败状态、cancelRefetch、hasNextPage 推导等行为)。

【免费下载链接】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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 8:35:10

ty 泛型可调用对象全解析:PEP 484 传统语法下的类型推断与特化

ty 泛型可调用对象全解析&#xff1a;PEP 484 传统语法下的类型推断与特化 【免费下载链接】ruff An extremely fast Python linter and code formatter, written in Rust. 项目地址: https://gitcode.com/GitHub_Trending/ru/ruff 本篇技术指南以 Ruff 仓库内置的类型检…

作者头像 李华
网站建设 2026/9/10 8:34:41

OpenCore Legacy Patcher 实战指南:让老 Mac 免费装上最新版 macOS

OpenCore Legacy Patcher 实战指南&#xff1a;让老 Mac 免费装上最新版 macOS 【免费下载链接】OpenCore-Legacy-Patcher Experience macOS just like before 项目地址: https://gitcode.com/GitHub_Trending/op/OpenCore-Legacy-Patcher OpenCore Legacy Patcher&…

作者头像 李华
网站建设 2026/9/10 8:34:38

激光测距模组选型指南:原理、参数与场景避坑全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 8:32:53

MCU数字电源控制实战:从Matlab补偿器到定点PID实现

1. 这不是“电源设计笔记”&#xff0c;而是MCU上跑通数字电源控制的实战手记 你手上那块STM32F407或者GD32E507开发板&#xff0c;真的只是在点灯、串口打印、跑FreeRTOS吗&#xff1f;我去年接手一个工业温控模块升级项目时&#xff0c;客户原话是&#xff1a;“老模拟电源板…

作者头像 李华
网站建设 2026/9/10 8:32:49

GD32 FPU启用全链路配置:从硬件使能到CMSIS-DSP加速

简介&#xff1a;本资源面向嵌入式开发工程师及GD32平台进阶学习者&#xff0c;聚焦浮点运算与数字信号处理能力的实战落地&#xff0c;系统解决GD32微控制器中FPU启用、CMSIS-DSP库集成与高效调用等核心问题。资源包共3个文件&#xff0c;含1个预编译浮点数学库&#xff08;ar…

作者头像 李华