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无法优雅处理的问题:
- 分页参数需要记忆与累积:每次加载新页时,需要用上一次返回的游标/页码作为下一次请求的参数,并且所有已加载的页要拼接成一个列表;
- "是否还有下一页/上一页"需要推导:该状态由
getNextPageParam/getPreviousPageParam的返回值决定,是触发"加载更多"按钮或 Intersection Observer 的依据; - 页数需要受控:数据无限增长会拖垮内存与渲染,
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"设计一致);而真正的"无限"逻辑全部封装在InfiniteQueryObserver与infiniteQueryBehavior中(见下文"底层原理")。
二、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 }) => ...)。 - 返回
undefined或null表示没有下一页。
典型用法(游标分页):
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。 - 返回
undefined或null表示没有上一页。
它服务于"向上翻页"(如聊天记录往上加载)场景。hasPreviousPage在未定义该函数或返回空值时均为false(见 infiniteQueryBehavior.ts)。
3.5 maxPages —— 页数上限
- 类型:
number | undefined,默认undefined。 - 无限查询数据中最多存储的页数。
- 当达到最大页数后,再获取新页会导致
pages数组头部或尾部被移除,具体取决于获取方向:- 向后加载(
fetchNextPage)时移除第一页(addToEnd,见 infiniteQueryBehavior.ts); - 向前加载(
fetchPreviousPage)时移除最后一页(addToStart)。
- 向后加载(
- 若为
undefined或0,页数不受限制。 - 若
maxPages > 0,必须正确定义getNextPageParam与getPreviousPageParam,以便在需要时两个方向都能拉取页面。
页的追加/裁剪由utils中的addToEnd/addToStart完成(对pages和pageParams同步操作,保证两者长度一致)。
使用建议:对"只增不减"的 Feed 流可保持
undefined;对内存敏感或需要固定窗口(如仅保留最近 N 页)的场景设置具体数值。要兼顾"向上加载"与"向下加载",同时启用maxPages时务必把两个 pageParam 函数都配齐。
四、Returns:返回属性详解
useInfiniteQuery返回一个 SolidJS Store(Store<InfiniteQueryObserverResult>),其属性与 useQuery hook 一致(含status、isPending、isSuccess、isError、dataUpdatedAt、error、failureCount、fetchStatus、refetch等),差异点在于:
data不是普通数据,而是{ pages, pageParams }结构(见 4.1);- 新增
fetchNextPage/fetchPreviousPage/hasNextPage/hasPreviousPage等 6 个方向性属性; isRefetching与isRefetchError的语义有细微差别(见 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 的isRefetching(isFetching && !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返回null时hasNextPage变为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,可配合Suspense与ErrorBoundary声明式处理首屏加载与错误(与 useQuery 的 Suspense 用法 相同,注意设置throwOnError: true):
<ErrorBoundary fallback={(err) => <div>出错了: {err.message}</div>}> <Suspense fallback={<div>加载中…</div>}> <ItemList /> </Suspense> </ErrorBoundary>六、底层原理:一次 fetchNextPage 调用发生了什么
把上面的 API 串起来,一次fetchNextPage()的完整链路是:
- Solid 层:
useInfiniteQuery把options(Accessor)交给useBaseQuery(见 useBaseQuery.ts),后者创建InfiniteQueryObserver实例并用createResource包装观察结果,返回带 Proxy 的 Store(对data做了 Resource 访问转发,见 useBaseQuery.ts)。 - Observer 层:
fetchNextPage()以meta.fetchMore.direction = 'forward'调用fetch(infiniteQueryObserver.ts)。 - Behavior 层:
infiniteQueryBehavior的onFetch读取direction与旧数据:- 有旧页且带方向 → 用
getNextPageParam(options, oldData)算出本次param,然后fetchPage(oldData, param)把新页追加到尾部(addToEnd),并受maxPages裁剪; - 无方向(首次/重取)→ 从
initialPageParam开始循环拉页直到达到remainingPages(infiniteQueryBehavior.ts)。
- 有旧页且带方向 → 用
- 结果合并:
createResult基于fetchDirection推导出isFetchingNextPage、isFetchNextPageError等 6 个方向性属性,并修正isRefetching/isRefetchError的语义(infiniteQueryObserver.ts)。 - 响应式传播:Store 更新后,组件中读取
state.data、state.isFetchingNextPage的地方自动重渲染。
这套设计的好处:方向状态不放在 Solid 层,而是内聚在 query-core 的 fetchMeta 中,因此useInfiniteQuery与useQuery的响应式基座完全共享,行为差异全部由InfiniteQueryObserver(setOptions时标记_type = 'infinite',见 infiniteQueryObserver.ts)与 Behavior 承担,Solid 包装层保持极薄。
七、注意事项与最佳实践
- 命令式翻页可能干扰默认重取:
fetchNextPage这类命令式调用会与默认的重取行为(如refetchOnWindowFocus、refetchInterval)相互干扰,可能导致展示过期数据。请仅在用户动作的响应中调用(点击、滚动触底回调),或加上hasNextPage && !isFetching之类的条件再调用。 - hasNextPage 依赖服务端返回:
getNextPageParam的返回值直接决定hasNextPage,因此服务端必须在每页响应中携带可判空的游标字段(如nextCursor: number | null)。 - maxPages 与双向加载:启用
maxPages且需要"向上/向下都能翻"时,务必同时定义getNextPageParam与getPreviousPageParam,否则裁剪方向会失去对侧可拉取的参数来源。 - 不要在 render 中直接调用 fetch 方法:
fetchNextPage应挂在事件回调(onClick)或createEffect中,避免渲染期间触发副作用。 - 服务端渲染注意:SSR 期间
useBaseQuery会把retry置为false、throwOnError置为true(见 useBaseQuery.ts),且fetchNextPage/fetchPreviousPage等函数在序列化时会被置空、水合后恢复(见 useBaseQuery.ts),因此 SSR 输出中不要依赖这些函数。 - 类型提示:若把配置对象抽到组件外,可借助
infiniteQueryOptions包装以获得data的InfiniteData类型推导(见 infiniteQueryOptions.ts)。
八、扩展阅读
- useQuery hook:
useInfiniteQuery的选项与返回值以它为基底,包含enabled、select、staleTime、gcTime、refetchInterval等全部通用配置的详细说明。 - Query Keys:
queryKey的哈希与自动更新机制。 - Query Functions:
QueryFunctionContext中各字段(含无限查询注入的pageParam、direction)的完整说明。 - 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),仅供参考