How to implement infinite scrolling with React Query

How to implement infinite scrolling with React Query

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

  1. fetchNextPage => function to be called to fetch next page.
  2. hasNextPage => boolean to indicate more data can be fetched.
  3. 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 pages
  • data.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.