分页查询 / 滞后查询
渲染分页数据是一种非常常见的 UI 模式,在 TanStack Query 中,只需将页码信息包含在查询键中,该模式便“开箱即用”:
TypeScript
1const projectsQuery = injectQuery(() => ({2 queryKey: ['projects', page()],3 queryFn: fetchProjects,4}))然而,如果你运行这个简单示例,可能会注意到某种异常现象:
UI 会在 success 和 pending 状态之间反复跳变,因为每个新页面都被视为一个全新的查询。
这种体验并不理想,不幸的是,当前许多工具仍强制采用这种方式工作。但 TanStack Query 并非如此!你可能已经猜到,TanStack Query 提供了一个强大的功能——placeholderData,可帮助我们规避这一问题。
使用 placeholderData 实现更优的分页查询
考虑如下示例:我们理想情况下希望为某个查询递增 pageIndex(或 cursor)。若仅使用 injectQuery,技术上依然可以正常工作,但每当为不同页面或 cursor 创建并销毁查询时,UI 仍会在 success 和 pending 状态之间跳变。而将 placeholderData 设为 TanStack Query 导出的 (previousData) => previousData 或 keepPreviousData 函数后,你将获得以下几项新能力:
- 即使查询键已变更,在新数据拉取完成前,上一次成功拉取的数据依然可用。
- 当新数据到达时,先前的
data会被无缝切换为展示新数据。 isPlaceholderData可用于获知当前查询实际提供的是哪部分数据。
angular-ts
1@Component({2 selector: 'pagination-example',3 template: `4 <div>5 <p>6 In this example, each page of data remains visible as the next page is7 fetched. The buttons and capability to proceed to the next page are also8 suppressed until the next page cursor is known. Each page is cached as a9 normal query too, so when going to previous pages, you'll see them10 instantaneously while they are also re-fetched invisibly in the11 background.12 </p>13 @if (projectsQuery.status() === 'pending') {14 <div>Loading...</div>15 } @else if (projectsQuery.status() === 'error') {16 <div>Error: {{ projectsQuery.error().message }}</div>17 } @else {18 <!-- 'data' will either resolve to the latest page's data -->19 <!-- or if fetching a new page, the last successful page's data -->20 <div>21 @for (project of projectsQuery.data().projects; track project.id) {22 <p>{{ project.name }}</p>23 }24 </div>25 }26 27 <div>Current Page: {{ page() + 1 }}</div>28 <button (click)="previousPage()" [disabled]="page() === 0">29 Previous Page30 </button>31 <button32 (click)="nextPage()"33 [disabled]="34 projectsQuery.isPlaceholderData() || !projectsQuery.data()?.hasMore35 "36 >37 Next Page38 </button>39 <!-- Since the last page's data potentially sticks around between page requests, -->40 <!-- we can use 'isFetching' to show a background loading -->41 <!-- indicator since our status === 'pending' state won't be triggered -->42 @if (projectsQuery.isFetching()) {43 <span> Loading...</span>44 }45 </div>46 `,47})48export class PaginationExampleComponent {49 page = signal(0)50 readonly #queryClient = inject(QueryClient)51 52 readonly projectsQuery = injectQuery(() => ({53 queryKey: ['projects', this.page()],54 queryFn: () => lastValueFrom(fetchProjects(this.page())),55 placeholderData: keepPreviousData,56 staleTime: 5 * 1000,57 }))58 59 constructor() {60 effect(() => {61 // Prefetch the next page!62 if (63 !this.projectsQuery.isPlaceholderData() &&64 this.projectsQuery.data()?.hasMore65 ) {66 void this.#queryClient67 .query({68 queryKey: ['projects', this.page() + 1],69 queryFn: () => lastValueFrom(fetchProjects(this.page() + 1)),70 })71 .catch(noop)72 }73 })74 }75 76 previousPage() {77 this.page.update((old) => Math.max(old - 1, 0))78 }79 80 nextPage() {81 this.page.update((old) =>82 this.projectsQuery.data()?.hasMore ? old + 1 : old,83 )84 }85}使用 placeholderData 对无限查询结果实现滞后处理
尽管使用场景相对较少,placeholderData 选项同样能完美适配 injectInfiniteQuery 函数,因此你可以让用户在无限查询键随时间变化的过程中,持续无缝地查看缓存数据。