无限查询
渲染可以持续“加载更多”数据到现有数据集上,或实现“无限滚动”的列表,是一种非常常见的 UI 模式。TanStack Query 提供了一个 useQuery 的有用变体,称为 useInfiniteQuery,用于查询这类列表。
使用 useInfiniteQuery 时,你会注意到一些不同之处:
data现在是一个包含无限查询数据的对象:data.pages数组,包含已获取的各个页面数据data.pageParams数组,包含用于获取这些页面的参数
现在提供了
fetchNextPage和fetchPreviousPage函数(fetchNextPage是必需的)现在提供了
initialPageParam选项(且为必需项),用于指定初始页面参数提供了
getNextPageParam和getPreviousPageParam选项,用于判断是否还有更多数据可加载,以及获取加载所需的信息。这些信息将作为额外参数传递给查询函数现在提供了一个
hasNextPage布尔值,如果getNextPageParam返回的值不是null或undefined,则为true现在提供了一个
hasPreviousPage布尔值,如果getPreviousPageParam返回的值不是null或undefined,则为true现在提供了
isFetchingNextPage和isFetchingPreviousPage布尔值,用于区分后台刷新状态和“加载更多”状态
注意:
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: [...] }根据这些信息,我们可以创建一个“加载更多”UI,方法如下:
让
useInfiniteQuery默认请求第一组数据在
getNextPageParam中返回下一次查询所需的信息调用
fetchNextPage函数
import { useInfiniteQuery } from '@tanstack/react-query'
function Projects() {
const fetchProjects = async ({ pageParam }) => {
const res = await fetch('/api/projects?cursor=' + pageParam)
return res.json()
}
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,
} = useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
return status === 'pending' ? (
<p>加载中...</p>
) : status === 'error' ? (
<p>错误:{error.message}</p>
) : (
<>
{data.pages.map((group, i) => (
<React.Fragment key={i}>
{group.data.map((project) => (
<p key={project.id}>{project.name}</p>
))}
</React.Fragment>
))}
<div>
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetching}
>
{isFetchingNextPage
? '正在加载...'
: hasNextPage
? '加载更多'
: '没有更多内容可加载'}
</button>
</div>
<div>{isFetching && !isFetchingNextPage ? '获取中...' : null}</div>
</>
)
}需要重点理解的是,如果在已有请求正在进行时调用 fetchNextPage,可能会覆盖正在后台刷新的数据。当渲染列表并同时触发 fetchNextPage 时,这种情况尤其关键。
请记住,一个 InfiniteQuery 同时只能有一个正在进行的请求。所有页面共享一个缓存条目,尝试同时发起两次请求可能会导致数据被覆盖。
如果你希望启用同时请求,可以在 fetchNextPage 中使用 { cancelRefetch: false } 选项(默认值为 true)。
为了确保查询过程无缝且无冲突,强烈建议在调用前检查查询是否处于 isFetching 状态,特别是当用户无法直接控制该调用时。
<List onEndReached={() => hasNextPage && !isFetching && fetchNextPage()} />当无限查询需要重新获取数据时会发生什么?
当一个无限查询变为“过期”状态并需要重新获取数据时,每个分组将按顺序从第一个开始重新获取。这确保了即使底层数据发生了变化,我们也不会使用过时的游标,从而避免重复或跳过记录。如果一个无限查询的结果从查询缓存中被移除,分页将重新回到初始状态,仅请求初始分组。
如何实现双向无限列表?
可以通过使用 getPreviousPageParam、fetchPreviousPage、hasPreviousPage 和 isFetchingPreviousPage 属性和函数来实现双向列表。
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(),
}),
})如何手动更新无限查询?
手动移除第一页:
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,
}))仅保留第一页:
queryClient.setQueryData(['projects'], (data) => ({
pages: data.pages.slice(0, 1),
pageParams: data.pageParams.slice(0, 1),
}))务必始终保留 pages 和 pageParams 的相同数据结构!
如何限制页面数量?
在某些用例中,你可能希望限制查询数据中存储的页面数量,以提高性能和用户体验:
当用户可以加载大量页面时(内存占用)
当你需要重新获取包含数十个页面的无限查询时(网络开销:所有页面将按顺序重新获取)
解决方案是使用“有限无限查询”(Limited Infinite Query)。这可以通过结合使用 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,
})如果我的 API 不返回游标怎么办?
如果您的 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
},
})进一步阅读
要更深入地了解无限查询的工作原理,请阅读社区资源中的文章 无限查询是如何工作的。
最后更新于
这有帮助吗?