Implementing infinite scrolling in React

Implementing infinite scrolling in React

Infinite scrolling is a web-design technique that loads content continuously as the user scrolls down the page. The infinite scrolling mechanism automatically retrieves data from an API when needed without requiring the user to perform an extra action.

In this article, we will learn how to implement the infinite scrolling technique in React apps. We will use a React hook to fetch random words from a REST API that provides a paginated API endpoint; then, we will build a component to render the list of random words retrieved from the REST API. We will also learn about the Intersection Observer API and how it can be used to determine when to retrieve more content.

Project Setup

To set up the project, run the following on your terminal:

npx create-react-app infinite-scrolling
cd infinite-scrolling
npm install axios
npm start

Let’s create a React hook that handles pagination and parses data from the REST API to get started. We’ll be using Axios for HTTP requests.

The REST API endpoint requires the following query parameters:

  • page: Indicates what page/offset to return data from.
  • limit: Indicates the number of items to be returned per page.

For example, returns the following:

    "success": true,
    "message": "Random words",
    "data": [
    "pageData": {
        "total": 100,
        "currentPage": 1,
        "nextPage": 2,
        "prevPage": null,
        "lastPage": 10

The pageData attribute contains some vital page information like:

  • nextPage: Specifies the value of the next page,
  • prevPage: Specifies the value of the previous page.
  • total: The total number of data items.

Now create a folder name hooks inside the src folder. Within this folder, create a file named usePagination.js and add the following code:

import axios from "axios";
import { useEffect, useState } from "react";

export default function usePagination(page) {
  const [data, setData] = useState([]); // stores data retrieved
  const [more, setMore] = useState(true); // indicates if there's more data to be retrieved from the API.
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(false); // indicates error existence

  useEffect(() => {
    // Reset error state before loading

    // indicate that data-fetching is ongoing

    // make call to API
    axios.get('', {
      params: { // query params
        limit: 10, 
    }).then(({ data: resData }) => {
      // add fetched data to list. We make use of Set to avoid duplicates
      setData((prev) => [ Set([...prev,])]); 
      setMore(Boolean(resData.pageData.nextPage)); // set more to true if nextPage exists
    .catch(() => setError(true)); // set error to true if an exception was caught
  }, [page]);

  return { data, more, loading, error };

The hook above fetches more content from the REST API when the value of page increases. This is done with the help of the useEffect hook that triggers the API call every time the value of page changes.

The hook returns useful data that the component will use. These include:

  • data: List of data fetched.
  • more: Indicates if there are more resources to be fetched.
  • loading: Indicates when data-fetching is ongoing.
  • error: Indicates if an error has occurred while fetching resources

Let’s now create our list component that uses the usePagination hook to fetch and display data (list of random words).

Edit the App.js file to contain the following:

import { useState, useRef, useCallback } from 'react';
import './App.css';
import usePagination from './hooks/usePagination';

function App() {
  const [page, setPage] = useState(1);

  // get data returned from hooks
  const { data, more, loading, error } = usePagination(page);
  // Observer

  return (
    <div className="App">
      <ul className="list-container">
        {/** Render list of words from data*/}
        {, index) => (
            // ref
        {/** if data-fetching is in progress, the indicator below is rendered */}
        {loading && (
          <li id="loader">Loading...</li>
      {/** if there's no more data to be fetched, render an indicator to inform the user  */}
      {!more && <div id="end">You've the reached the end</div>}

export default App;

We added the usePagination hook and a page state in the list component. Whenever the value of page changes, the usePagination hook will be fired, and data for the new page will be fetched from the API.

Two elements are also rendered conditionally. The loader element gets rendered when loading is true to inform the user that more data is being fetched. The div element with an id of end is rendered when there is no more data to be fetched from the REST API (i.e., when more is false).

Also, edit App.css and add the following CSS rules:

.list-container {
  padding: 0;
  margin: 0;
  list-style: none;

.list-item {
  padding: 12px;
  cursor: pointer;

.list-item:hover {
  background: rgba(0, 0, 0, .05);

#loader {
  color: rgba(0, 0, 235, 0.5);
  padding: 12px;
  cursor: progress;

#end {
  padding: 12px;
  font-weight: 700;

If you head on to your browser, you’ll find that our app displays a list of 10 words. This is the first page of data from the REST API (10 data items are returned because we specified a limit of 10 in the query):


How does the component retrieve more content from the API when the user scrolls to the last item in the list? This is where Intersection Observer API comes in. Let’s dig a little into it.

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.


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

What is the Intersection Observer API?

The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document’s viewport. It can be used to determine the visibility of an element in relation to another element or the device’s viewport. This means that it can be used to detect when an element becomes visible on the user’s screen/viewport or within an element.

How then can the Intersection Observer API be used to implement infinite scrolling?

To implement infinite scrolling:

  • The Intersection Observer API can determine when the last element in the list becomes visible in the viewport or the parent element.
  • At this point, we can increment the value of page; this will trigger the usePagination hook to fetch more items from the source.
  • This cycle is repeated until the last set of data is retrieved from the source (in this case, the REST API).

Let’s get into creating the observer. We’ll use a React callback ref and IntersectionObserver API to keep track of the last element in the list. Edit the App.js file, and add the following code to the App component. Just under the // Observer comment:

// Observer
const observer = useRef(); // ref to store observer
const lastElementRef = useCallback((element) => { 
  //element is the react element being referenced
  // disconnect observer set on previous last element
  if (observer.current) observer.current.disconnect();
  // if there's no more data to be fetched, don't set new observer
  if (!more) return;
  // set new observer
  observer.current = new IntersectionObserver((entries) => {
    // increase page number when element enters (is intersecting with) viewport.
    // This triggers the pagination hook to fetch more items in the new page
    if (entries[0].isIntersecting && more) setPage((prev) => prev + 1);

  // observe/monitor last element
  if (element) observer.current.observe(element);
}, [more]);

Here’s a break-down of the code above:

  • A ref is used to store the observer because we want the observer variable to be mutable. We also use the useCallback hook with more as a dependency to prevent re-creation of the function on every re-render.
  • Inside the callback, we disconnect the observer monitoring the previous last element and set a new observer to watch the current last element. Note that the function parameter element refers to the element referenced using the ref prop.
  • Inside the IntersectionObserver callback, a check is made to see if the first entry (the element being monitored) intersects with the viewport (visible in the viewport) and if there are more items to be fetched. If these conditions are true, the value of page is incremented. This triggers the usePagination hook to fetch more items for the new page.

Finally, let’s add the lastElementRef to the last element in the list. Edit the App component and add the conditional ref to the list component so that it looks like this:

  // ref
  ref={index === data.length - 1 ? lastElementRef : undefined}

From the code above, the lastElementRef is set on the last element in the list by checking if the index equals that of the last item in the array.

When we run the app on the browser, we get the following result:



In this article, you learned how to implement infinite scrolling in React apps. We looked at the Intersection Observer API and how it can be used to implement infinite scrolling.

A TIP FROM THE EDITOR: React Query also provides an alternative: check out our Infinite scrolling with React Query article. And, if you are curious about solutions with other frameworks, don’t miss the Infinite Scrolling in Angular and Infinite Scrolling in Vue using the Vue Intersection Observer API articles.