4. Pagination and Infinite Scrolling
Pagination is a common interface design pattern. Request parameters typically carry pagination parameters like limit/page, and the return value usually has this structure:
{
cur_page: 1,
total: 500,
data: []
}
It seems that by properly controlling queryKey, useQuery can perfectly handle pagination requirements.
However, every time you navigate to a new page to fetch new data, the key changes, causing data to become empty and resulting in page flickering.
The first argument of placeholderData is prevData. We can take advantage of this: instead of returning empty data directly when loading new data, we can keep the data from the previous page. Combined with a simple semi-transparent filter effect to indicate loading, we can ensure responsiveness while reducing page flickering.
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>
Usually we want to disable the navigation buttons when new data is loading.
const { isPlaceholderData } = useBooksQuery(sort, page)
const prevDisabled = page === 1 || page === data.total || isPlaceholderData
const nextDisabled = page === data.total || isPlaceholderData
Combined with the prefetch introduced in the previous article, we can implement preloading of the next page's data.
Note the
useEffectsection: here we preload the next page's data via thequeryClient.prefetchQuerymethod.
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()
// Preload the next page
useEffect(() => {
queryClient.prefetchQuery(getBooksQueryOptions(sort, page + 1))
}, [sort, page, queryClient])
return useQuery(getBooksQueryOptions(sort, page))
}
Infinite Scrolling
Infinite scrolling usually means automatically loading the next page's data when the user scrolls to the bottom.
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>
The above definition is not accurate, because there is also the possibility of automatically loading the previous page's data when the user scrolls upward. So infinite scrolling needs to support bidirectional data fetching (for example, fetching chat history).
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 }
},
})
Notably, the data returned by useInfiniteQuery is no longer the data returned by queryFn, but an array of data called pages.
pages represents the collection of data from all pages fetched so far.
The refetch returned by useInfiniteQuery will trigger a re-request for all pages, not just the current page.
This is because when data on one page changes, subsequent (or preceding) pages may also change accordingly.