logo

分页加载与无限滚动 —— react-query 【4】分页

Authors
  • avatar
    Name
    White Play
    Twitter

黄叶与书法

黄叶与书法

分页是常见的接口设计模式,请求参数通常会携带limit/page等分页参数,返回值通常是这种结构:

{
  cur_page: 1,
  total: 500,
  data: []
}

似乎平常的useQuery基于对queryKey的合理控制,就可以完美解决分页的需求。

但每次翻页获取新数据都会因为 key 的改变,使data变成空数据,导致页面抖动。

placeholderData的第一个参数是 prevData,我们可以利用这一点。让加载新数据时不再直接返回空数据,而是保留上一页的数据,配合一个简单的半透明滤镜效果表明正在加载就可以在保证有响应的同时减少页面抖动。

const {data,isPlaceholderData} = useQuery({
	...,
	placeholderData: (prevData) => {
		return prevData
	}
})

//...some tsx code...
<ul style={{ opacity: isPlaceholderData ? 0.5 : 1}}>
 {data.map(item => <li>{item.name}</li>)}
</ul>

通常我们会想在新数据加载时禁用翻页按钮。

const { isPlaceholderData } = useBooksQuery(sort, page)

const prevDisabled = page === 1 || page === data.total || isPlaceholderData
const nextDisabled = page === data.total || isPlaceholderData

结合上一篇文章介绍的prefetch,实现预加载下一页的数据。

注意useEffect部分,这里通过queryClient.prefetchQuery方法预加载下一页的数据。

const getBooksQueryOptions = (sort: string, page: number) => ({
  queryKey: ['books', sort, page],
  queryFn: () => fetchBooks(sort, page),
  staleTime: 1000 * 60 * 5,
})

const useBooksQuery = (sort: string, page: number) => {
  const queryClient = useQueryClient()

  // 预加载下一页
  useEffect(() => {
    queryClient.prefetchQuery(getBooksQueryOptions(sort, page + 1))
  }, [sort, page, queryClient])

  return useQuery(getBooksQueryOptions(sort, page))
}

无限滚动

无限滚动通常是当用户滚动到底部时,自动加载下一页的数据。

const { data, hasNextPage, fetchNextPage } = useInfiniteQuery({
  queryKey: ["books", sort],
  initialPageParam: 1,
  queryFn: ({ pageParam = 1 }) => fetchBooks(sort, pageParam),
  getNextPageParam: (lastPage, pages, lastPageParam) => {
    if (lastPage.cur_page === lastPage.total) {
      return undefined;
    }
    return lastPage.cur_page + 1;
  },
});

//...some tsx code...
<ul>
  {data.pages.map((page) => (
    <li>{page.data.map((item) => item.name)}</li>
  ))}
</ul>
<button onClick={() => fetchNextPage()} disabled={!hasNextPage}>
  Next Page
</button>

上述定义是不准确的,因为还有一种可能是用户向上滚动时,自动加载上一页的数据。所以无限滚动需要支持双向获取数据(比如获取聊天记录)

useInfiniteQuery({
  queryKey: ['books', sort],
  initialPageParam: { page: 1 },
  queryFn: ({ pageParam = 1 }) => fetchBooks(sort, pageParam),
  getNextPageParam: ({ lastPage, allPages }) => {
    if (lastPage.cur_page === lastPage.total) {
      return undefined
    }
    return { page: allPages.length + 1 }
  },
  getPreviousPageParam: ({ lastPage, allPages }) => {
    if (lastPage.cur_page === 1) {
      return undefined
    }
    return { page: allPages.length - 1 }
  },
})

值得注意的是,useInfiniteQuery返回值的data不再不是queryFn返回的数据,而是由数据组成的数组,名字叫pages

pages 表示目前已获取的所有页的数据的集合。

useInfiniteQuery返回的refetch将会触发所有页面的重新请求,而非仅触发当前页码的重新请求。

这是因为当某一页数据发生变更,可能后续(或前面)的每一页都随着发生变更。