在 UI 中渲染分頁資料是非常常見的模式,而在 TanStack Query 中,只需將頁面資訊包含在查詢鍵 (query key) 中即可「直接運作」:
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) 或游標 (cursor)。如果使用 injectQuery,技術上仍能正常運作,但當為每個頁面或游標創建和銷毀不同查詢時,UI 仍會在 success 和 pending 狀態之間跳動。通過將 placeholderData 設為 (previousData) => previousData 或使用 TanStack Query 導出的 keepPreviousData 函數,我們可以獲得以下新特性:
@Component({
selector: 'pagination-example',
template: `
<div>
<p>
在此範例中,當獲取下一頁資料時,當前頁面的資料仍保持可見。按鈕和前往下一頁的功能也會在下一頁游標未知時被禁用。每一頁都會像普通查詢一樣被緩存,因此當返回上一頁時,你會立即看到它們,同時它們也會在背景無形中被重新獲取。
</p>
@if (query.status() === 'pending') {
<div>載入中...</div>
} @else if (query.status() === 'error') {
<div>錯誤: {{ query.error().message }}</div>
} @else {
<!-- 'data' 要麼解析為最新頁面的資料 -->
<!-- 要麼在獲取新頁面時,顯示最後一次成功的頁面資料 -->
<div>
@for (project of query.data().projects; track project.id) {
<p>{{ project.name }}</p>
}
</div>
}
<div>當前頁面: {{ page() + 1 }}</div>
<button (click)="previousPage()" [disabled]="page() === 0">
上一頁
</button>
<button
(click)="nextPage()"
[disabled]="query.isPlaceholderData() || !query.data()?.hasMore"
>
下一頁
</button>
<!-- 由於最後一頁的資料可能在頁面請求之間持續存在 -->
<!-- 我們可以使用 'isFetching' 來顯示背景載入指示器 -->
<!-- 因為 status === 'pending' 狀態不會被觸發 -->
@if (query.isFetching()) {
<span> 載入中...</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(() => {
// 預先獲取下一頁!
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>
在此範例中,當獲取下一頁資料時,當前頁面的資料仍保持可見。按鈕和前往下一頁的功能也會在下一頁游標未知時被禁用。每一頁都會像普通查詢一樣被緩存,因此當返回上一頁時,你會立即看到它們,同時它們也會在背景無形中被重新獲取。
</p>
@if (query.status() === 'pending') {
<div>載入中...</div>
} @else if (query.status() === 'error') {
<div>錯誤: {{ query.error().message }}</div>
} @else {
<!-- 'data' 要麼解析為最新頁面的資料 -->
<!-- 要麼在獲取新頁面時,顯示最後一次成功的頁面資料 -->
<div>
@for (project of query.data().projects; track project.id) {
<p>{{ project.name }}</p>
}
</div>
}
<div>當前頁面: {{ page() + 1 }}</div>
<button (click)="previousPage()" [disabled]="page() === 0">
上一頁
</button>
<button
(click)="nextPage()"
[disabled]="query.isPlaceholderData() || !query.data()?.hasMore"
>
下一頁
</button>
<!-- 由於最後一頁的資料可能在頁面請求之間持續存在 -->
<!-- 我們可以使用 'isFetching' 來顯示背景載入指示器 -->
<!-- 因為 status === 'pending' 狀態不會被觸發 -->
@if (query.isFetching()) {
<span> 載入中...</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(() => {
// 預先獲取下一頁!
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 函數完美配合,讓你能在無限查詢鍵隨時間變化的同時,無縫地讓使用者繼續查看緩存的資料。