Back

Infinite scrolling with React Query

Infinite scrolling with React Query

Fetching and rendering large amounts of data at once could have negative effects like performance issues leading to a bad user experience. To effectively work with large data, two patterns are mainly used, pagination and infinite scrolling, both dealing with rendering or fetching small chunks of data at a time.

In this tutorial, we will learn how to implement infinite scrolling in React using React Query. To follow up with this tutorial, you should be familiar with React, React hooks, and React Query.

Introduction to Infinite scrolling

Infinite scrolling is a pattern used in apps to load content continuously as the user scrolls to the bottom of the page. It’s most useful for loading large data sets from a server in chunks to improve performance by reducing the time taken to fetch and render content. Social media sites like Facebook, Twitter, etc., use this to display feeds. Other sites with user-generated content, like YouTube, also implement this pattern.

Aside from infinite scrolling, there is another design pattern for loading large data sets in chunks: pagination. But unlike infinite scrolling with pagination, users must click a button to load the following content when they scroll to the bottom of the page. Both patterns have their use cases. Infinite scrolling is better suited for exploring content, where users are browsing aimlessly for something interesting, which is not the case for pagination.

Implementing infinite scrolling in React

In this tutorial, we will be using the GitHub Search API, which allows us to search for items on GitHub. The API can fetch many items in one request or small chunks depending on the parameters passed to it. We will use it to search for repositories on GitHub. Here is an example of the API URL to be used in this tutorial: https://api.github.com/search/repositories?q=topic:react&per_page=5&page=1. This will get the first page containing five React repositories on GitHub. (Note that this API call is a clear example of pagination!)

I have already created a starter repo where I have added the functionality to search and display React repositories on GitHub using React Query, so we can focus on how to implement infinite scrolling. Clone the repo with the following commands:

git clone -b starter https://github.com/mandah-israel/infinite-scrolling.git
cd infinite-scrolling
npm i

Now, when we start the app using $ npm start, we will see the following screen:

Our basic screen

React Query provides a hook for infinite scrolling, useInfiniteQuery. It’s similar to the [useQuery](https://react-query.tanstack.com/reference/useQuery) hook with a few differences:

  • The returned data is now an object containing two array properties. One is pageParams access with data.pageParams, an array containing the page params used to fetch the pages. The other is pages access with data.pages, an array containing the fetched pages.
  • The options passed as the third parameter include getNextPageParam and getPreviousPageParam to determine if there is more data to load.
  • fetchNextPage and fetchPreviousPage functions are included as returned properties for fetching the next and previous pages, respectively.
  • hasNextPage and hasPreviousPage boolean properties are returned to determine if there is a next or previous page.
  • isFetchingNextPage and isFetchingPreviousPage boolean properties are returned to check when the next page or previous page is fetching.

We only need to use a few options/properties above to implement infinite scrolling. We will first replace the useQuery hook with useInfiniteQuery and also update the function used to fetch the data in our starter app. To do this, head over to the App.js file and add the following import:

// App.js
import { useInfiniteQuery } from "react-query"

Next, in the App component replace fetchRepositories and useQuery with the following lines of code:

// App.js`
function App() {
  const LIMIT = 10
  
  const fetchRepositories = async (page) => {
    const response = await fetch(`https://api.github.com/search/repositories?q=topic:react&per_page=${LIMIT}&page=${page}`)
    return response.json()
  }
  
  const {data, isSuccess, hasNextPage, fetchNextPage, isFetchingNextPage} = useInfiniteQuery(
    'repos', 
    ({pageParam = 1}) => fetchRepositories(pageParam),
    {
      getNextPageParam: (lastPage, allPages) => {
        const nextPage = allPages.length + 1
        return nextPage 
      }
    }
  )

...

In the above code, we have modified the API URL by adding page and per_page parameters, which will fetch only ten repositories per page. We have replaced the useQuery hook with useInfiniteQuery, making available the needed properties. We are also adding the getNextPageParam option, which receives the last page of the infinite list of data and the array of all pages that have been fetched. This function either returns a value to be used to get the following data or undefined indicating data is no longer available. We are returning the following page param in the function, which will be used when the fetchNextPage function is called to get the next page.

Next, let’s modify how we are accessing and displaying our data. In the return statement, modify the div with the app class name to look like this:

// App.js
<div className="app">
  {isSuccess && data.pages.map(page => 
    page.items.map((comment) => (
      <div className='result' key={comment.id}>
        <span>{comment.name}</span>
        <p>{comment.description}</p>
      </div>
    ))
  )}
</div>

With this, the fetched results should now be displayed. For Infinite scrolling to start working, we need to call the fetchNextPage function any time we scroll to the bottom of the page. To see when we get to the bottom of the page, we can do that using either the browser Scroll event or the Intersection Observer API. We will cover both of them in the following sections.

Open Source Session Replay

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

replayer.png

Start enjoying your debugging experience - start using OpenReplay for free.

Fetching new data using scroll events

In the App.js file, first add the following import:

    // App.js
    import { useEffect } from 'react';

Next, add the following lines of code in the App component after the useInfiniteQuery hook:

// App.js
useEffect(() => {
  let fetching = false;
  const handleScroll = async (e) => {
    const {scrollHeight, scrollTop, clientHeight} = e.target.scrollingElement;
    if(!fetching && scrollHeight - scrollTop <= clientHeight * 1.2) {
      fetching = true
      if(hasNextPage) await fetchNextPage()
      fetching = false
    }
  }
  document.addEventListener('scroll', handleScroll)
  return () => {
    document.removeEventListener('scroll', handleScroll)
  }
}, [fetchNextPage, hasNextPage])

In the above code, we added a scroll event listener to the document, which calls the handleScroll function when fired. In it, we detect when we get to the bottom of the page using the scrollingEvent property, then call fetchNextPage to fetch the next page if hasNextPage is true.

Right now, hasNextPage will always be true because in the getNextPageParam option, we are returning a param which is the value to get the next page. For hasNextPage to be false, we need to return undefined or any other falsy value in getNextPageParam. We will do this later to create the functionality to stop fetching data events after scrolling to the bottom.

With this, new data will be fetched when we start our app and scroll close to the bottom of the page.

Fetching new data using Intersection Observer

The Intersection Observer API provides a way to observe the visibility and position of a DOM element relative to the containing root element or viewport. Simply put, it monitors when an observed element is visible or reaches a predefined position and fires the callback function supplied to it. Using this API to implement the infinite scrolling functionality, we will first create an element at the bottom of our fetched data which will be the observed element. Then when this element is visible, we will call the fetchNextPage function. Let’s do that.

First, add the following imports to the App.js file:

import {useRef, useCallback} from 'react'

Next, add the following lines of code before the closing tag (</div>) of the div with the className of app.

// App.js
<div className="app">
  ...
  <div className='loader' ref={observerElem}>
    {isFetchingNextPage && hasNextPage ? 'Loading...' : 'No search left'}
  </div>
</div>

Above we created the div element we want to observe using Intersection Observers. We have added the ref attribute to access it directly. The above div will display Loading… or No search left depending on isFetchingNextPage and hasNextPage boolean values.

Next, add the following line of code at the top of the App component:

// App.js
function App() {
  const observerElem = useRef(null)
...

Here we have created the observerElem variable that was passed on to the ref attribute. With this, when the DOM loads, we can access the div element we created above. We are doing this to pass the div element to the Intersection Observer from our code. Next, add the following lines of code after the useInfiniteQuery hook.

// App.js
const handleObserver = useCallback((entries) => {
  const [target] = entries
  if(target.isIntersecting) {
    fetchNextPage()
  }
}, [fetchNextPage, hasNextPage])

useEffect(() => {
  const element = observerElem.current
  const option = { threshold: 0 }

  const observer = new IntersectionObserver(handleObserver, option);
  observer.observe(element)
  return () => observer.unobserve(element)
}, [fetchNextPage, hasNextPage, handleObserver])

Above, we created a handleObserver function which is the callback passed to IntersectionObserver. It calls fetchNextPage when the target element specified with observer.observe(element) has entered the viewport.

With this, new data will be fetched when we scroll to the bottom of the page in our app.

Control fetching depending on available data

Right now, even when there is no data left to fetch, and we scroll to the bottom of the page in our app, the fetchNextPage will still be called, sending requests to the API to get more data. To prevent this, we need to return a false value (undefined, 0 null, false) in getNextPageParam when no data is left. This way, the hasNextPage returned property will be equal to false when there is no data left, then we will use it to control when the fetchNextPage function is called.

To do this, modify the getNextPageParam option of the useInfiniteQuery hook to look like this:

// App.js
getNextPageParam: (lastPage, allPages) => {
  const nextPage = allPages.length + 1
  return lastPage.items.length !== 0 ? nextPage : undefined
}

Above, we are returning the following page param or undefined based on whether data was returned in the last fetch made.

Now let’s modify the handleObserver function to call fetchNextPage only when hasNextPage equals false. Here is what it will look like:

// App.js
const handleObserver = useCallback((entries) => {
  const [target] = entries
  if(target.isIntersecting && hasNextPage) {
    fetchNextPage()
  }
}, [fetchNextPage, hasNextPage])

With this, we are done implementing the infinite scrolling functionality.

Conclusion

Infinite scrolling displays large sets of data in chunks, reducing the rendering time of fetched data. This tutorial taught us how to implement it with the scrolling events and the Intersection Observer API when using React Query.