在 TanStack Query 中,实现加载更多数据或无限滚动的功能是一种常见的 UI 模式。为此,useInfiniteQuery
钩子可以用于查询这类数据列表,并提供了许多有用的特性来处理分页和数据加载。
使用 useInfiniteQuery 查询
与 useQuery
不同,useInfiniteQuery
返回的数据是一个对象,其中包含以下内容:
data.pageParams
:存储用于获取各页面的参数。
fetchNextPage
和 fetchPreviousPage
:分别用于获取下一页和上一页数据。
getNextPageParam
和 getPreviousPageParam
:这两个选项用于判断是否还有更多数据需要加载,以及获取下一页或上一页的数据。
示例:基本的无限查询
假设我们的 API 返回每页 3 个项目,并使用一个游标来获取下一组数据:
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) => lastPage.nextCursor // 获取下一页的参数
});
return status === "pending" ? (
<p>Loading...</p>
) : status === "error" ? (
<p>Error: {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 || isFetchingNextPage}>
{isFetchingNextPage ? "Loading more..." : hasNextPage ? "Load More" : "Nothing more to load"}
</button>
</div>
<div>{isFetching && !isFetchingNextPage ? "Fetching..." : null}</div>
</>
);
}
关键点:
hasNextPage
:如果返回值不是 null
或 undefined
,则表示有下一页数据。
isFetchingNextPage
:标识是否正在加载下一页的数据。
处理同时进行的请求:
在发起 fetchNextPage
请求时,如果有正在进行的请求(例如刷新数据),可能会导致数据覆盖。为了避免这种情况,您可以使用 { cancelRefetch: false }
选项来允许同时发起多个请求,但如果不是必需,最好避免在正在请求数据时发起其他请求。
<List onEndReached={() => !isFetchingNextPage && fetchNextPage()} />
无限查询的重取操作:
当一个无限查询变得过时并需要重新获取时,数据将按顺序重新请求,从第一页开始。这可以确保即使底层数据发生变化,我们也不会使用过时的游标,避免出现重复或漏掉记录的情况。如果查询缓存中的数据被移除,分页将重新开始,只有初始组会被请求
双向无限列表
如果您想实现一个双向无限列表,可以使用 getPreviousPageParam
、fetchPreviousPage
、hasPreviousPage
和 isFetchingPreviousPage
属性和函数
useInfiniteQuery({
queryKey: ["projects"],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
getPreviousPageParam: (firstPage) => 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
}));
限制存储的页面数量:
如果您希望在查询数据中限制存储的页面数量,可以使用 maxPages
选项:
useInfiniteQuery({
queryKey: ["projects"],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
getPreviousPageParam: (firstPage) => firstPage.prevCursor,
maxPages: 3 // 只保存最多3页数据
});
没有游标的 API:
如果您的 API 不返回游标,可以将 pageParam
用作游标,通过 getNextPageParam
和 getPreviousPageParam
计算下一个或上一个页面的参数:
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;
}
});
通过这些特性,您可以根据需求灵活地实现分页、无限滚动以及双向加载的功能。