---
id: query.infinite-cursor
kind: recipe
product: query
framework: react
locale: zh-CN
revision: 1
sourceCheckedOn: "2026-08-03"
versionRange: "^5"
verifiedAgainst: "@tanstack/react-query@5.101.4"
contentModel: 2
packages:
  - "@tanstack/react-query"
tasks:
  - "build-infinite-query"
  - "paginate-by-cursor"
  - "bound-query-pages"
sourceRefs:
  - "https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries"
  - "https://tanstack.com/query/latest/docs/framework/react/reference/useInfiniteQuery"
---

# 建立有界的游标 Infinite Query

> 用一份 queryKey 管理整个列表，让服务端游标驱动 pageParam，并用 maxPages 控制缓存与重取成本。

## 执行契约

- **适用场景:** 一个列表需要追加加载页面，并由服务端游标或明确 offset 决定下一页。
- **不要使用:** 普通分页需要可分享页码和随机跳页时，优先把 page 放入 URL 并使用常规 Query。
- **前置检查:** 定义 initialPageParam、末页信号、稳定 Query Key、页面响应形状以及是否需要 maxPages 或双向翻页。
- **验证结果:** 记录连续 pageParam，验证末页、失败重试、筛选切换与重复点击；pages 和 pageParams 必须始终一一对应。
- **失败模式:** 重复页通常来自客户端猜测游标、getNextPageParam 返回旧值或并发触发 fetchNextPage；从网络记录和最后一页响应定位。
- **安全边界:** 游标是外部输入且可能被篡改；服务端必须验证作用域、排序与用户权限，不能把游标当作授权令牌。

## 输入契约

确定第一页 pageParam、响应中的 nextCursor、末页信号以及筛选条件。筛选属于 queryKey，游标属于 pageParam。后端若使用 offset，也要从 lastPageParam 明确推导，不能依赖组件中的局部页码。

## 最小实现

initialPageParam 与 getNextPageParam 在 v5 中为必需契约。返回 undefined 结束列表；用 AbortSignal 停止过期列表请求。maxPages 大于 0 时，双向翻页还必须提供 getPreviousPageParam。

File: `src/features/issues/queries.ts`

```ts
import { infiniteQueryOptions } from '@tanstack/react-query'

export const issueFeedOptions = infiniteQueryOptions({
  queryKey: ['issues', 'feed'] as const,
  initialPageParam: null as string | null,
  queryFn: ({ pageParam, signal }) => fetchIssuePage(pageParam, signal),
  getNextPageParam: (page) => page.nextCursor ?? undefined,
  maxPages: 5,
})
```

## 缓存与交互检查

渲染前 flatten data.pages，但写缓存时保留 `{ pages, pageParams }`。加载按钮由 hasNextPage 与 isFetchingNextPage 共同控制；测试末页、失败重试、筛选切换、快速重复触发以及 maxPages 淘汰方向。

## Official sources

- https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries
- https://tanstack.com/query/latest/docs/framework/react/reference/useInfiniteQuery
