渲染可以累加“加载更多”数据到现有数据集或“无限滚动”的列表也是一种非常常见的 UI 模式。TanStack Query 支持一个有用的 useQuery 版本,称为 useInfiniteQuery,用于查询这些类型的列表。
当使用 useInfiniteQuery 时,您会注意到一些不同的地方
注意:initialData 或 placeholderData 选项需要符合具有 data.pages 和 data.pageParams 属性的对象的相同结构。
假设我们有一个 API,它基于 cursor 索引一次返回 3 个 projects 页面,以及可用于获取下一组项目的游标
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:
<script setup>
import { useInfiniteQuery } from '@tanstack/vue-query'
const fetchProjects = async ({ pageParam = 0 }) => {
const res = await fetch('/api/projects?cursor=' + pageParam)
return res.json()
}
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
isPending,
isError,
} = useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
</script>
<template>
<span v-if="isPending">Loading...</span>
<span v-else-if="isError">Error: {{ error.message }}</span>
<div v-else-if="data">
<span v-if="isFetching && !isFetchingNextPage">Fetching...</span>
<ul v-for="(group, index) in data.pages" :key="index">
<li v-for="project in group.projects" :key="project.id">
{{ project.name }}
</li>
</ul>
<button
@click="() => fetchNextPage()"
:disabled="!hasNextPage || isFetchingNextPage"
>
<span v-if="isFetchingNextPage">Loading more...</span>
<span v-else-if="hasNextPage">Load More</span>
<span v-else>Nothing more to load</span>
</button>
</div>
</template>
<script setup>
import { useInfiniteQuery } from '@tanstack/vue-query'
const fetchProjects = async ({ pageParam = 0 }) => {
const res = await fetch('/api/projects?cursor=' + pageParam)
return res.json()
}
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
isPending,
isError,
} = useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
</script>
<template>
<span v-if="isPending">Loading...</span>
<span v-else-if="isError">Error: {{ error.message }}</span>
<div v-else-if="data">
<span v-if="isFetching && !isFetchingNextPage">Fetching...</span>
<ul v-for="(group, index) in data.pages" :key="index">
<li v-for="project in group.projects" :key="project.id">
{{ project.name }}
</li>
</ul>
<button
@click="() => fetchNextPage()"
:disabled="!hasNextPage || isFetchingNextPage"
>
<span v-if="isFetchingNextPage">Loading more...</span>
<span v-else-if="hasNextPage">Load More</span>
<span v-else>Nothing more to load</span>
</button>
</div>
</template>
务必理解,在正在进行获取时调用 fetchNextPage 存在覆盖后台数据刷新的风险。当渲染列表并同时触发 fetchNextPage 时,这种情况尤其关键。
请记住,对于 InfiniteQuery,只能有一个正在进行的获取。单个缓存条目在所有页面之间共享,尝试同时获取两次可能会导致数据覆盖。
如果您打算启用同步获取,您可以在 fetchNextPage 中使用 { cancelRefetch: false } 选项(默认值:true)。
为了确保无冲突的无缝查询过程,强烈建议验证查询是否未处于 isFetching 状态,特别是当用户不会直接控制该调用时。
<List onEndReached={() => !isFetchingNextPage && fetchNextPage()} />
<List onEndReached={() => !isFetchingNextPage && fetchNextPage()} />
当无限查询变为 stale 并且需要重新获取时,每个组都 按顺序 获取,从第一个开始。这确保即使底层数据发生突变,我们也不会使用过时的游标,并可能获得重复项或跳过记录。如果无限查询的结果从 queryCache 中移除,则分页将以初始状态重新开始,仅请求初始组。
双向列表可以通过使用 getPreviousPageParam、fetchPreviousPage、hasPreviousPage 和 isFetchingPreviousPage 属性和函数来实现。
useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
})
useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
})
有时您可能希望以相反的顺序显示页面。如果是这种情况,您可以使用 select 选项
useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
select: (data) => ({
pages: [...data.pages].reverse(),
pageParams: [...data.pageParams].reverse(),
}),
})
useInfiniteQuery({
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),
}))
确保始终保持页面和 pageParams 的相同数据结构!
在某些用例中,您可能希望限制存储在查询数据中的页面数量,以提高性能和 UX
解决方案是使用“有限无限查询”。这可以通过结合使用 maxPages 选项与 getNextPageParam 和 getPreviousPageParam 来实现,以允许在需要时在两个方向上获取页面。
在以下示例中,查询数据 pages 数组中仅保留 3 个页面。如果需要重新获取,则只会按顺序重新获取 3 个页面。
useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
maxPages: 3,
})
useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
maxPages: 3,
})
如果您的 API 不返回游标,您可以将 pageParam 用作游标。因为 getNextPageParam 和 getPreviousPageParam 也获取当前页面的 pageParam,您可以使用它来计算下一个/上一个页面参数。
return useInfiniteQuery({
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
},
})
return useInfiniteQuery({
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
},
})