渲染可以累加地“加载更多”数据到现有数据集或“无限滚动”的列表也是一个非常常见的 UI 模式。TanStack Query 支持一个名为 injectInfiniteQuery 的 injectQuery 的有用版本,用于查询这些类型的列表。
在使用 injectInfiniteQuery 时,您会注意到一些不同之处:
注意:initialData 或 placeholderData 选项需要具有与具有 data.pages 和 data.pageParams 属性的对象相同的结构。
假设我们有一个 API,它基于 cursor 索引每次返回 3 页 projects,以及一个可用于获取下一组项目的 cursor。
fetch('/api/projects?cursor=0')
// { data: [...], nextCursor: 3}
fetch('/api/projects?cursor=3')
// { data: [...], nextCursor: 6}
fetch('/api/projects?cursor=6')
// { data: [...], nextCursor: 9}
fetch('/api/projects?cursor=9')
// { data: [...] }
fetch('/api/projects?cursor=0')
// { data: [...], nextCursor: 3}
fetch('/api/projects?cursor=3')
// { data: [...], nextCursor: 6}
fetch('/api/projects?cursor=6')
// { data: [...], nextCursor: 9}
fetch('/api/projects?cursor=9')
// { data: [...] }
有了这些信息,我们可以通过以下方式创建一个“加载更多”UI:
import { Component, computed, inject } from '@angular/core'
import { injectInfiniteQuery } from '@tanstack/angular-query-experimental'
import { lastValueFrom } from 'rxjs'
import { ProjectsService } from './projects-service'
@Component({
selector: 'example',
templateUrl: './example.component.html',
})
export class Example {
projectsService = inject(ProjectsService)
query = injectInfiniteQuery(() => ({
queryKey: ['projects'],
queryFn: async ({ pageParam }) => {
return lastValueFrom(this.projectsService.getProjects(pageParam))
},
initialPageParam: 0,
getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined,
getNextPageParam: (lastPage) => lastPage.nextId ?? undefined,
maxPages: 3,
}))
nextButtonDisabled = computed(
() => !this.#hasNextPage() || this.#isFetchingNextPage(),
)
nextButtonText = computed(() =>
this.#isFetchingNextPage()
? 'Loading more...'
: this.#hasNextPage()
? 'Load newer'
: 'Nothing more to load',
)
#hasNextPage = this.query.hasNextPage
#isFetchingNextPage = this.query.isFetchingNextPage
}
import { Component, computed, inject } from '@angular/core'
import { injectInfiniteQuery } from '@tanstack/angular-query-experimental'
import { lastValueFrom } from 'rxjs'
import { ProjectsService } from './projects-service'
@Component({
selector: 'example',
templateUrl: './example.component.html',
})
export class Example {
projectsService = inject(ProjectsService)
query = injectInfiniteQuery(() => ({
queryKey: ['projects'],
queryFn: async ({ pageParam }) => {
return lastValueFrom(this.projectsService.getProjects(pageParam))
},
initialPageParam: 0,
getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined,
getNextPageParam: (lastPage) => lastPage.nextId ?? undefined,
maxPages: 3,
}))
nextButtonDisabled = computed(
() => !this.#hasNextPage() || this.#isFetchingNextPage(),
)
nextButtonText = computed(() =>
this.#isFetchingNextPage()
? 'Loading more...'
: this.#hasNextPage()
? 'Load newer'
: 'Nothing more to load',
)
#hasNextPage = this.query.hasNextPage
#isFetchingNextPage = this.query.isFetchingNextPage
}
<div>
@if (query.isPending()) {
<p>Loading...</p>
} @else if (query.isError()) {
<span>Error: {{ query?.error().message }}</span>
} @else { @for (page of query?.data().pages; track $index) { @for (project of
page.data; track project.id) {
<p>{{ project.name }} {{ project.id }}</p>
} }
<div>
<button (click)="query.fetchNextPage()" [disabled]="nextButtonDisabled()">
{{ nextButtonText() }}
</button>
</div>
}
</div>
<div>
@if (query.isPending()) {
<p>Loading...</p>
} @else if (query.isError()) {
<span>Error: {{ query?.error().message }}</span>
} @else { @for (page of query?.data().pages; track $index) { @for (project of
page.data; track project.id) {
<p>{{ project.name }} {{ project.id }}</p>
} }
<div>
<button (click)="query.fetchNextPage()" [disabled]="nextButtonDisabled()">
{{ nextButtonText() }}
</button>
</div>
}
</div>
理解在进行中的 fetch 正在进行时调用 fetchNextPage 有覆盖后台数据刷新的风险至关重要。在渲染列表并同时触发 fetchNextPage 时,这种情况尤其关键。
请记住,InfiniteQuery 只能有一个正在进行的 fetch。所有页面共享一个缓存条目,同时尝试两次 fetch 可能会导致数据覆盖。
如果您打算启用同时 fetch,可以在 fetchNextPage 中使用 { cancelRefetch: false } 选项(默认值:true)。
为了确保无缝的查询过程而没有冲突,强烈建议验证查询是否处于 isFetching 状态,特别是如果用户不会直接控制该调用。
@Component({
template: ` <list-component (endReached)="fetchNextPage()" /> `,
})
export class Example {
query = injectInfiniteQuery(() => ({
queryKey: ['projects'],
queryFn: async ({ pageParam }) => {
return lastValueFrom(this.projectsService.getProjects(pageParam))
},
}))
fetchNextPage() {
// Do nothing if already fetching
if (this.query.isFetching()) return
this.query.fetchNextPage()
}
}
@Component({
template: ` <list-component (endReached)="fetchNextPage()" /> `,
})
export class Example {
query = injectInfiniteQuery(() => ({
queryKey: ['projects'],
queryFn: async ({ pageParam }) => {
return lastValueFrom(this.projectsService.getProjects(pageParam))
},
}))
fetchNextPage() {
// Do nothing if already fetching
if (this.query.isFetching()) return
this.query.fetchNextPage()
}
}
当无限查询变为 stale 并需要重新 fetch 时,每个组都是 sequentially (顺序地) fetch 的,从第一个开始。这确保了即使底层数据被更改,我们也不会使用过时的游标并可能获取重复项或跳过记录。如果无限查询的结果被从 queryCache 中移除,分页将以初始状态重新开始,只请求初始组。
可以通过使用 getPreviousPageParam、fetchPreviousPage、hasPreviousPage 和 isFetchingPreviousPage 属性和函数来实现双向列表。
query = injectInfiniteQuery(() => ({
queryKey: ['projects'],
queryFn: fetchProjects,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
}))
query = injectInfiniteQuery(() => ({
queryKey: ['projects'],
queryFn: fetchProjects,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
}))
有时你可能希望以相反的顺序显示页面。如果是这种情况,你可以使用 select 选项。
query = injectInfiniteQuery(() => ({
queryKey: ['projects'],
queryFn: fetchProjects,
select: (data) => ({
pages: [...data.pages].reverse(),
pageParams: [...data.pageParams].reverse(),
}),
}))
query = injectInfiniteQuery(() => ({
queryKey: ['projects'],
queryFn: fetchProjects,
select: (data) => ({
pages: [...data.pages].reverse(),
pageParams: [...data.pageParams].reverse(),
}),
}))
queryClient.setQueryData(['projects'], (data) => ({
pages: data.pages.slice(1),
pageParams: data.pageParams.slice(1),
}))
queryClient.setQueryData(['projects'], (data) => ({
pages: data.pages.slice(1),
pageParams: data.pageParams.slice(1),
}))
const newPagesArray =
oldPagesArray?.pages.map((page) =>
page.filter((val) => val.id !== updatedId),
) ?? []
queryClient.setQueryData(['projects'], (data) => ({
pages: newPagesArray,
pageParams: data.pageParams,
}))
const newPagesArray =
oldPagesArray?.pages.map((page) =>
page.filter((val) => val.id !== updatedId),
) ?? []
queryClient.setQueryData(['projects'], (data) => ({
pages: newPagesArray,
pageParams: data.pageParams,
}))
queryClient.setQueryData(['projects'], (data) => ({
pages: data.pages.slice(0, 1),
pageParams: data.pageParams.slice(0, 1),
}))
queryClient.setQueryData(['projects'], (data) => ({
pages: data.pages.slice(0, 1),
pageParams: data.pageParams.slice(0, 1),
}))
请务必始终保持页面和页面参数的相同数据结构!
在某些用例中,你可能希望限制查询数据中存储的页面数量,以提高性能和用户体验
解决方案是使用“有限无限查询”。这可以通过结合 maxPages 选项与 getNextPageParam 和 getPreviousPageParam 来实现,从而允许根据需要在两个方向上 fetch 页面。
在以下示例中,查询数据页面数组中只保留 3 个页面。如果需要重新获取,将只按顺序重新获取 3 个页面。
injectInfiniteQuery(() => ({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
maxPages: 3,
}))
injectInfiniteQuery(() => ({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
maxPages: 3,
}))
如果您的 API 不返回游标,您可以使用 pageParam 作为游标。因为 getNextPageParam 和 getPreviousPageParam 也会接收当前页的 pageParam,您可以使用它来计算下一个/上一个 pageParam。
injectInfiniteQuery(() => ({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam) => {
if (lastPage.length === 0) {
return undefined
}
return lastPageParam + 1
},
getPreviousPageParam: (firstPage, allPages, firstPageParam) => {
if (firstPageParam <= 1) {
return undefined
}
return firstPageParam - 1
},
}))
injectInfiniteQuery(() => ({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam) => {
if (lastPage.length === 0) {
return undefined
}
return lastPageParam + 1
},
getPreviousPageParam: (firstPage, allPages, firstPageParam) => {
if (firstPageParam <= 1) {
return undefined
}
return firstPageParam - 1
},
}))
要更好地理解无限查询在底层是如何工作的,请阅读社区资源中的 无限查询是如何工作的。