渲染分页数据是一种非常常见的 UI 模式,在 TanStack Query 中,通过在查询键中包含页面信息,它可以“正常工作”
const result = injectQuery(() => ({
queryKey: ['projects', page()],
queryFn: fetchProjects,
}))
const result = injectQuery(() => ({
queryKey: ['projects', page()],
queryFn: fetchProjects,
}))
但是,如果你运行这个简单的示例,你可能会注意到一些奇怪的事情
UI 会在 success 和 pending 状态之间跳动,因为每个新页面都被视为一个全新的查询。
这种体验不是最佳的,不幸的是,这是当今许多工具坚持工作的方式。但 TanStack Query 不是!正如你可能已经猜到的,TanStack Query 附带一个很棒的功能,称为 placeholderData,它允许我们绕过这个问题。
考虑以下示例,我们理想情况下希望为查询递增 pageIndex(或游标)。如果我们使用 injectQuery,它在技术上仍然可以正常工作,但 UI 会在 success 和 pending 状态之间跳动,因为为每个页面或游标创建和销毁不同的查询。通过将 placeholderData 设置为 (previousData) => previousData 或从 TanStack Query 导出的 keepPreviousData 函数,我们可以获得一些新功能
@Component({
selector: 'pagination-example',
template: `
<div>
<p>
In this example, each page of data remains visible as the next page is
fetched. The buttons and capability to proceed to the next page are also
suppressed until the next page cursor is known. Each page is cached as a
normal query too, so when going to previous pages, you'll see them
instantaneously while they are also re-fetched invisibly in the
background.
</p>
@if (query.status() === 'pending') {
<div>Loading...</div>
} @else if (query.status() === 'error') {
<div>Error: {{ query.error().message }}</div>
} @else {
<!-- 'data' will either resolve to the latest page's data -->
<!-- or if fetching a new page, the last successful page's data -->
<div>
@for (project of query.data().projects; track project.id) {
<p>{{ project.name }}</p>
}
</div>
}
<div>Current Page: {{ page() + 1 }}</div>
<button (click)="previousPage()" [disabled]="page() === 0">
Previous Page
</button>
<button
(click)="nextPage()"
[disabled]="query.isPlaceholderData() || !query.data()?.hasMore"
>
Next Page
</button>
<!-- Since the last page's data potentially sticks around between page requests, -->
<!-- we can use 'isFetching' to show a background loading -->
<!-- indicator since our status === 'pending' state won't be triggered -->
@if (query.isFetching()) {
<span> Loading...</span>
}
</div>
`,
})
export class PaginationExampleComponent {
page = signal(0)
queryClient = inject(QueryClient)
query = injectQuery(() => ({
queryKey: ['projects', this.page()],
queryFn: () => lastValueFrom(fetchProjects(this.page())),
placeholderData: keepPreviousData,
staleTime: 5000,
}))
constructor() {
effect(() => {
// Prefetch the next page!
if (!this.query.isPlaceholderData() && this.query.data()?.hasMore) {
this.#queryClient.prefetchQuery({
queryKey: ['projects', this.page() + 1],
queryFn: () => lastValueFrom(fetchProjects(this.page() + 1)),
})
}
})
}
previousPage() {
this.page.update((old) => Math.max(old - 1, 0))
}
nextPage() {
this.page.update((old) => (this.query.data()?.hasMore ? old + 1 : old))
}
}
@Component({
selector: 'pagination-example',
template: `
<div>
<p>
In this example, each page of data remains visible as the next page is
fetched. The buttons and capability to proceed to the next page are also
suppressed until the next page cursor is known. Each page is cached as a
normal query too, so when going to previous pages, you'll see them
instantaneously while they are also re-fetched invisibly in the
background.
</p>
@if (query.status() === 'pending') {
<div>Loading...</div>
} @else if (query.status() === 'error') {
<div>Error: {{ query.error().message }}</div>
} @else {
<!-- 'data' will either resolve to the latest page's data -->
<!-- or if fetching a new page, the last successful page's data -->
<div>
@for (project of query.data().projects; track project.id) {
<p>{{ project.name }}</p>
}
</div>
}
<div>Current Page: {{ page() + 1 }}</div>
<button (click)="previousPage()" [disabled]="page() === 0">
Previous Page
</button>
<button
(click)="nextPage()"
[disabled]="query.isPlaceholderData() || !query.data()?.hasMore"
>
Next Page
</button>
<!-- Since the last page's data potentially sticks around between page requests, -->
<!-- we can use 'isFetching' to show a background loading -->
<!-- indicator since our status === 'pending' state won't be triggered -->
@if (query.isFetching()) {
<span> Loading...</span>
}
</div>
`,
})
export class PaginationExampleComponent {
page = signal(0)
queryClient = inject(QueryClient)
query = injectQuery(() => ({
queryKey: ['projects', this.page()],
queryFn: () => lastValueFrom(fetchProjects(this.page())),
placeholderData: keepPreviousData,
staleTime: 5000,
}))
constructor() {
effect(() => {
// Prefetch the next page!
if (!this.query.isPlaceholderData() && this.query.data()?.hasMore) {
this.#queryClient.prefetchQuery({
queryKey: ['projects', this.page() + 1],
queryFn: () => lastValueFrom(fetchProjects(this.page() + 1)),
})
}
})
}
previousPage() {
this.page.update((old) => Math.max(old - 1, 0))
}
nextPage() {
this.page.update((old) => (this.query.data()?.hasMore ? old + 1 : old))
}
}
虽然不常见,但 placeholderData 选项也可以与 injectInfiniteQuery 函数完美配合,因此你可以无缝地允许用户在无限查询键随时间变化时继续查看缓存的数据。