无限查询

在 TanStack Query 中,实现加载更多数据或无限滚动的功能是一种常见的 UI 模式。为此,useInfiniteQuery 钩子可以用于查询这类数据列表,并提供了许多有用的特性来处理分页和数据加载。

使用 useInfiniteQuery 查询

useQuery 不同,useInfiniteQuery 返回的数据是一个对象,其中包含以下内容:

  • data.pages:存储所有已获取的页面数据。

  • data.pageParams:存储用于获取各页面的参数。

  • fetchNextPagefetchPreviousPage:分别用于获取下一页和上一页数据。

  • getNextPageParamgetPreviousPageParam:这两个选项用于判断是否还有更多数据需要加载,以及获取下一页或上一页的数据。

示例:基本的无限查询

假设我们的 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>
    </>
  );
}

关键点:

  • data.pages:存储所有已获取的数据页。

  • fetchNextPage:用于请求下一页数据。

  • hasNextPage:如果返回值不是 nullundefined,则表示有下一页数据。

  • isFetchingNextPage:标识是否正在加载下一页的数据。

处理同时进行的请求:

在发起 fetchNextPage 请求时,如果有正在进行的请求(例如刷新数据),可能会导致数据覆盖。为了避免这种情况,您可以使用 { cancelRefetch: false } 选项来允许同时发起多个请求,但如果不是必需,最好避免在正在请求数据时发起其他请求。

<List onEndReached={() => !isFetchingNextPage && fetchNextPage()} />

无限查询的重取操作:

当一个无限查询变得过时并需要重新获取时,数据将按顺序重新请求,从第一页开始。这可以确保即使底层数据发生变化,我们也不会使用过时的游标,避免出现重复或漏掉记录的情况。如果查询缓存中的数据被移除,分页将重新开始,只有初始组会被请求

双向无限列表

如果您想实现一个双向无限列表,可以使用 getPreviousPageParamfetchPreviousPagehasPreviousPageisFetchingPreviousPage 属性和函数

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 用作游标,通过 getNextPageParamgetPreviousPageParam 计算下一个或上一个页面的参数:

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;
  }
});

通过这些特性,您可以根据需求灵活地实现分页、无限滚动以及双向加载的功能。

最后更新于

这有帮助吗?