Infinite scrolling with React Query
Tags: react, webdev, javascript
Published: 2022-06-20 21:12:00

by [Mandah Israel](https://blog.openreplay.com/authors/mandah-israel)


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](https://en.wiktionary.org/wiki/pagination) and [infinite scrolling](https://en.wiktionary.org/wiki/infinite_scroll), 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](https://react-query.tanstack.com/). 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](https://docs.github.com/en/rest/search), 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](https://blog.openreplay.com/static/d99458b09c1ec2dd7ef2faa8b8b3ddff/897d4/image01.png)

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:

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

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

```javascript
// 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:

```javascript
// 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](https://developer.mozilla.org/en-US/docs/Web/API/Document/scroll_eventhttps://developer.mozilla.org/en-US/docs/Web/API/Document/scroll_event) or the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). We will cover both of them in the following sections.

## Open Source Session Replay

_[OpenReplay](https://github.com/openreplay/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](https://static.openreplay.com/images/banner-blog.png)

_Start enjoying your debugging experience - [start using OpenReplay for free](https://github.com/openreplay/openreplay)._



## Fetching new data using scroll events

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

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

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

```javascript
// 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](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) 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:

```javascript
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.`

```javascript
// 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:

```javascript
// 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.

```javascript
// 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:

```javascript
// 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:

```javascript
// 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.

[![newsletter](https://blog.openreplay.com/static/e6b18eda7a327511296a2f27692f9c70/e9957/newsletter.png)](https://newsletter.openreplay.com/)