Why Infinite Scrolling?
Infinite scroll is almost everywhere (YouTube, Instagram, Twitter, etc.). So using pagination makes our app belong to the past. In this article, I'll be sharing my learning on how to create an infinite scrolling list with React Query.
Where did I use it?
I am building a movie discovery and watch list app called Flixplore using the TMDB API. The API limits the number of results to 20 movies per response. So I had to implement infinite scrolling for the movie listing component.
Using useInfiniteQuery
React Query has a special hook especially for infinite scrolling or pagination called useInfiniteQuery
. It is similar to useQuery
but few things are different. In addition to {data, error, status}
we will be getting(/needing) three more properties
fetchNextPage
=> function to be called to fetch next page.hasNextPage
=> boolean to indicate more data can be fetched.isFetchingNextPage
=> boolean to indicate loading.
const fetchMovies= useCallback(
({ pageParam = 1 }) => API.movies({ genre: genre.id, page: pageParam }), [genre],
);
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
status,
} = useInfiniteQuery(
"movies",
fetchMovies,
{
getNextPageParam: (lastPage) => {
const { page, total_pages: totalPages } = lastPage.data;
return (page < totalPages) ? page + 1 : undefined;
},
}
);
Note that we are passing a function called getNextPageParam
as an option.
This function returns the information to the consecutive query. The parameter lastPage
contains the response of the last query, with which we can determine the page parameter to be passed to the query to fetch the consecutive data.
Another difference that useInfiniteQuery
brings to the table is that the data
now does not contain only the query response. Its structure is as follows
data.pages
array containing the fetched pagesdata.pageParams
array containing the page params used to fetch the pages
So, we need to render a list of pages and a list of movies on each page. (A nested loop)
{data.pages.map((page) => (
<Fragment key={page.data.page}>
{page.data.results.map((movie) => (
<MovieTile
key={movie.id}
movie={movie}
/>
))}
</Fragment>
))}
Here we are using Fragment
instead of <></>
because we need to pass a key to our list item, in this case, the page itself.
Load more using IntersectionObserver
All that's left to do is call the fetchNextPage
function when the user scrolls to the bottom. Lets us add a button called Load More
at the bottom of the list and attach a ref
to it. So, when the button intersects with the bottom of the viewport we need to call fetchNextPage
. Also, for large screens, all of the first query data might fit into one page. For this edge case, we need to call fetchNextPage
on the button's click event.
const loadMoreButtonRef = useRef();
<button
ref={loadMoreButtonRef}
onClick={() => fetchNextPage()}
>
Load More
</button>
Now we will be observing the intersection of the button with the viewport and call the fetchNextPage
function.
useEffect(() => {
if (!hasNextPage) {
return;
}
const observer = new IntersectionObserver(
(entries) => entries.forEach((entry) => {
if (entry.isIntersecting) {
fetchNextPage();
}
}),
);
const el = loadMoreButtonRef && loadMoreButtonRef.current;
if (!el) {
return;
}
observer.observe(el);
return () => {
observer.unobserve(el);
};
}, [loadMoreButtonRef.current, hasNextPage]);
The entries
contain a list of the attached targets. The isIntersecting
property is a boolean which denotes if the target element is currently intersecting with the root element (in our case, the viewport). The observe
and unobserve
function attaches and detaches the IntersectionObserver with the target element.
React Query docs has a CodeSandbox example for the same.