译零 TransZero · TanStack Query 中文文档
主站

无限查询

更新于 2026-11-02 Angular 查看英文原文 ↗

渲染可增量“加载更多”数据到现有数据集或实现“无限滚动”的列表,也是一种非常常见的 UI 模式。TanStack Query 支持一种适用于此类列表的实用版 injectQuery,称为 injectInfiniteQuery。

使用 injectInfiniteQuery 时,你会注意到以下几点差异:

  • data 现在是一个包含无限查询数据的对象:
  • data.pages 数组,包含已拉取的页面
  • data.pageParams 数组,包含用于拉取各页面的页面参数
  • fetchNextPage 和 fetchPreviousPage 函数现在可用(其中 fetchNextPage 是必需的)
  • initialPageParam 选项现在可用(且为必需项),用于指定初始页面参数
  • getNextPageParam 和 getPreviousPageParam 选项可用于判断是否还有更多数据可加载,以及获取加载所需的信息。该信息将作为额外参数传入查询函数
  • 现在提供一个 hasNextPage 布尔值,当 getNextPageParam 返回值不为 null 或 undefined 时,其值为 true
  • 现在提供一个 hasPreviousPage 布尔值,当 getPreviousPageParam 返回值不为 null 或 undefined 时,其值为 true
  • 现在提供 isFetchingNextPage 和 isFetchingPreviousPage 布尔值,以区分后台刷新状态与“加载更多”状态

注意:initialData 或 placeholderData 选项需符合具有 data.pages 和 data.pageParams 属性的对象结构。

示例

假设我们有一个 API,它根据 cursor 索引每次返回 projects 3 条,并附带一个可用于获取下一组项目的游标:

TypeScript
1fetch('/api/projects?cursor=0')2// { data: [...], nextCursor: 3}3fetch('/api/projects?cursor=3')4// { data: [...], nextCursor: 6}5fetch('/api/projects?cursor=6')6// { data: [...], nextCursor: 9}7fetch('/api/projects?cursor=9')8// { data: [...] }

基于此信息,我们可通过以下方式构建“加载更多” UI:

  • 默认等待 injectInfiniteQuery 请求第一组数据
  • 在 getNextPageParam 中返回下一次查询所需的信息
  • 调用 fetchNextPage 函数
angular-ts
1import { Component, computed, inject } from '@angular/core'2import { injectInfiniteQuery } from '@tanstack/angular-query-experimental'3import { lastValueFrom } from 'rxjs'4import { ProjectsService } from './projects-service'5 6@Component({7  selector: 'example',8  templateUrl: './example.component.html',9})10export class Example {11  readonly projectsService = inject(ProjectsService)12 13  readonly projectsQuery = injectInfiniteQuery(() => ({14    queryKey: ['projects'],15    queryFn: async ({ pageParam }) => {16      return lastValueFrom(this.projectsService.getProjects(pageParam))17    },18    initialPageParam: 0,19    getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined,20    getNextPageParam: (lastPage) => lastPage.nextId ?? undefined,21    maxPages: 3,22  }))23 24  nextButtonDisabled = computed(25    () => !this.#hasNextPage() || this.#isFetchingNextPage(),26  )27  nextButtonText = computed(() =>28    this.#isFetchingNextPage()29      ? 'Loading more...'30      : this.#hasNextPage()31        ? 'Load newer'32        : 'Nothing more to load',33  )34 35  #hasNextPage = this.projectsQuery.hasNextPage36  #isFetchingNextPage = this.projectsQuery.isFetchingNextPage37}
angular-html
1<div>2  @if (projectsQuery.isPending()) {3    <p>Loading...</p>4  } @else if (projectsQuery.isError()) {5    <span>Error: {{ projectsQuery?.error().message }}</span>6  } @else {7    @for (page of projectsQuery?.data().pages; track $index) {8      @for (project of page.data; track project.id) {9        <p>{{ project.name }} {{ project.id }}</p>10      }11    }12    <div>13      <button14        (click)="projectsQuery.fetchNextPage()"15        [disabled]="nextButtonDisabled()"16      >17        {{ nextButtonText() }}18      </button>19    </div>20  }21</div>

必须理解的是,在已有拉取正在进行时调用 fetchNextPage,存在覆盖后台正在进行的数据刷新的风险。当渲染列表并同时触发 fetchNextPage 时,这一情况尤为关键。

请记住,一个 InfiniteQuery 只能存在一个正在进行的拉取。所有页面共享单个缓存条目;若尝试同时发起两次拉取,可能导致数据被覆盖。

若你希望启用并发拉取,可在 fetchNextPage 中使用 { cancelRefetch: false } 选项(默认值:true)。

为确保查询过程无缝且无冲突,强烈建议验证查询是否处于 isFetching 状态,尤其是当用户无法直接控制该调用时。

angular-ts
1@Component({2  template: ` <list-component (endReached)="fetchNextPage()" /> `,3})4export class Example {5  readonly projectsQuery = injectInfiniteQuery(() => ({6    queryKey: ['projects'],7    queryFn: async ({ pageParam }) => {8      return lastValueFrom(this.projectsService.getProjects(pageParam))9    },10  }))11 12  fetchNextPage() {13    // Do nothing if already fetching14    if (this.projectsQuery.isFetching()) return15    this.projectsQuery.fetchNextPage()16  }17}

当无限查询需要重新拉取时会发生什么?

当无限查询变为 stale 并需要重新拉取时,各组数据将按顺序 sequentially 拉取,从第一组开始。这确保了即使底层数据被修改,我们也不会使用过期游标,从而避免重复或跳过记录。如果无限查询的结果曾从 queryCache 中移除,则分页将重置为初始状态,仅请求初始组数据。

如果我想实现双向无限列表怎么办?

可通过使用 getPreviousPageParam、fetchPreviousPage、hasPreviousPage 和 isFetchingPreviousPage 属性与函数来实现双向列表。

TypeScript
1projectsQuery = injectInfiniteQuery(() => ({2  queryKey: ['projects'],3  queryFn: fetchProjects,4  initialPageParam: 0,5  getNextPageParam: (lastPage, pages) => lastPage.nextCursor,6  getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,7}))

如果我想以倒序显示页面怎么办?

有时你可能希望以倒序显示页面。若如此,可使用 select 选项:

TypeScript
1projectsQuery = injectInfiniteQuery(() => ({2  queryKey: ['projects'],3  queryFn: fetchProjects,4  select: (data) => ({5    pages: [...data.pages].reverse(),6    pageParams: [...data.pageParams].reverse(),7  }),8}))

如果我想手动更新无限查询怎么办?

手动移除第一页:

TypeScript
1queryClient.setQueryData(['projects'], (data) => ({2  pages: data.pages.slice(1),3  pageParams: data.pageParams.slice(1),4}))

手动从某一页中移除单个值:

TypeScript
1const newPagesArray =2  oldPagesArray?.pages.map((page) =>3    page.filter((val) => val.id !== updatedId),4  ) ?? []5 6queryClient.setQueryData(['projects'], (data) => ({7  pages: newPagesArray,8  pageParams: data.pageParams,9}))

仅保留第一页:

TypeScript
1queryClient.setQueryData(['projects'], (data) => ({2  pages: data.pages.slice(0, 1),3  pageParams: data.pageParams.slice(0, 1),4}))

请务必始终维持页面和 pageParams 的相同数据结构!

如果我想限制页面数量怎么办?

在某些使用场景中,你可能希望限制查询数据中存储的页面数量,以提升性能和用户体验:

  • 当用户可加载大量页面时(内存占用)
  • 当你需要重新拉取包含数十页的无限查询时(网络开销:所有页面将被顺序拉取)

解决方案是使用“受限无限查询”(Limited Infinite Query)。这可通过结合使用 maxPages 选项与 getNextPageParam 和 getPreviousPageParam 实现,从而支持在两个方向上按需拉取页面。

在以下示例中,查询数据的 pages 数组仅保留 3 页。若需重新拉取,也仅会顺序重新拉取这 3 页。

TypeScript
1injectInfiniteQuery(() => ({2  queryKey: ['projects'],3  queryFn: fetchProjects,4  initialPageParam: 0,5  getNextPageParam: (lastPage, pages) => lastPage.nextCursor,6  getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,7  maxPages: 3,8}))

如果我的 API 不返回游标怎么办?

如果你的 API 不返回游标,可将 pageParam 用作游标。由于 getNextPageParam 和 getPreviousPageParam 同样接收当前页的 pageParam,你可以据此计算出下一页 / 上一页参数。

TypeScript
1injectInfiniteQuery(() => ({2  queryKey: ['projects'],3  queryFn: fetchProjects,4  initialPageParam: 0,5  getNextPageParam: (lastPage, allPages, lastPageParam) => {6    if (lastPage.length === 0) {7      return undefined8    }9    return lastPageParam + 110  },11  getPreviousPageParam: (firstPage, allPages, firstPageParam) => {12    if (firstPageParam <= 1) {13      return undefined14    }15    return firstPageParam - 116  },17}))

进一步阅读

要更深入地了解无限查询在底层的工作原理,请参阅文章 无限查询的工作原理。

本页为 TanStack Query 文档(Angular 版)的非官方中文翻译,原文由上游团队维护,遵循 MIT 许可。 译文如有偏差,以英文原文为准。

译自上游 59d11de