Working with Infinite Scroll in React

Most of the e-commerce sites displays a large number of data. And displaying the data at once will cost a major performance issue. To avoid the performance issue, we can use the pagination or infinite scrolling to load more data.

In this article, we are going to learn how to implement infinite scrolling in React. Infinite scrolling is technic to load more data on scrolling. Infinite scrolling is handier as per the user’s point of view because in pagination user has to click for the next page to load more data.

Implementing Infinite Scrolling

First of all, we need a dummy data API to set up the infinite scrolling. And JSON Placeholder APIs are good suite for us and it’s free.

The purpose of implementing infinite scrolling from scratch is to get familiar with the mechanism. Otherwise, some great libraries still exist for infinite scroll such as react-infinite-scroll-component.

Infinite Scroll

Let’s start creating the application with CRA ( create-react-app ). And later install the axios package. We required axios to fetch the data through API.

npx create-react-app infinite-scroll
npm install axios

You can add inline style or use any third-party design library with your project. We have used tailwindcss. We have a separate article on how to use tailwindcss with React.

Let’s start by setting states that we required to handle the scrolling event.

import React from 'react'
import './App.css';
import axios from 'axios'

const TOTAL_PAGES = 10;

const Loader = () => {
    return (
        <div className="w-full md:w-3/5 mx-auto p-4 text-center mb-4">
            <svg class="animate-spin h-8 w-8 mx-auto text-purple-700" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
                <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
            </svg>
        </div>
    )
}

function App() {

    const [items, setItems]         = React.useState([]);
    const [isLoading, setIsLoading] = React.useState(false);
    const [hasMore, setHasMore]     = React.useState(true);
    const [pages, setPages]         = React.useState(1);

}

The TOTAL_PAGES hold the total number of pages. And next, we have created a Loader which will show during the API calls.

The next part is the states and as the name suggests the items state holds the number of items that we will display, isLoading state return the boolean value and the Loader will be displayed based on this state.

The hasMore state will be used to check whether we have more data fetch or not. And the pages state is the current number of pages to request the API call and should be less than TOTAL_PAGES.

Now, we can use the componentDidMount() lifecycle method for the class-based component but as you can see that we are using hooks so useEffect() is working similar to componentDidMount().

function App() {

    const [items, setItems]         = React.useState([]);
    const [isLoading, setIsLoading] = React.useState(false);
    const [hasMore, setHasMore]     = React.useState(true);
    const [pages, setPages]         = React.useState(1);

    React.useEffect(() => {
        getItems(pages);
        setPages((pages) => pages + 1);
    }, []);

}

The getItems() function is responsible for fetching data from the API and incrementing the number of pages through setPages() by taking the previous value and incrementing by adding one.

Let’s create the getItems() function.

const getItems = async (page) => {
    setIsLoading(true);
    await new Promise((resolve) => setTimeout(resolve, 1000));

    await axios.get(`https://jsonplaceholder.typicode.com/photos?_page=${page}&_limit=5`)
    .then(resp => {
        setItems([...items, ...resp.data])
        setIsLoading(false)
    });
}

In the above code, our function takes the number of pages and uses it to the API to fetch the new data. First, we need to change the isLoading state by using the setIsLoading(), so that we can make the Loader visible during API calls.

Please ignore the setTimeout() function, we used it to display the loader.

Now let’s display the data through collection through API.

function App() {

    const [items, setItems]         = React.useState([]);
    const [isLoading, setIsLoading] = React.useState(false);
    const [hasMore, setHasMore]     = React.useState(true);
    const [pages, setPages]         = React.useState(1);

    React.useEffect(() => {
        getItems(pages);
        setPages((pages) => pages + 1);
    }, []);

    const getItems = async (page) => {
        setIsLoading(true);
        await new Promise((resolve) => setTimeout(resolve, 1000));

        await axios.get(`https://jsonplaceholder.typicode.com/photos?_page=${page}&_limit=5`)
        .then(resp => {
            setItems([...items, ...resp.data])
            setIsLoading(false)
        });
    }

    return (
        <>
            <div className="container mx-auto px-4">
                <div className="flex justify-center p-4 mb-4">
                    <h1 className="text-4xl font-semibold">React Infinite Scroll</h1>
                </div>
                <div className="flex flex-col">
                
                    {items.map((item, index) =>
                        (
                        <div className="w-full md:w-3/5 bg-gray-300 mx-auto p-4 rounded mb-4 flex">
                            <img src={item.thumbnailUrl} className="flex-auto mr-4" width="150" height="150" />
                            <div className="flex-auto">
                                <h2 className="text-2xl font-semibold mb-2">{item.title}</h2>
                                <p className="text-sm">
                                Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset ...
                                </p>
                            </div>
                        </div>
                        )
                    )}
                    
                    {isLoading && <Loader />}
                </div>
            </div>
        </>
    );
}

export default App;

Now start your application and check if everything working fine. You should see you application similar to the below image.

React Infinite Scroll

Hurray. But wait, we still need to implement to load the data on page scroll. And to implement the infinite scrolling we are going to use the useCallback() hook and Intersection Observer API.

Let’s start by creating a reference node. Open your App.js file and start editing.

import React from 'react'
import './App.css';
import axios from 'axios'

const TOTAL_PAGES = 10;

const Item = ({ children, reference }) => {
    return (
        <div ref={reference}>
            {children}
        </div>
    );
};

const Loader = () => {
    return (
        ....

        ....
    )
}


function App() {

    const [items, setItems]         = React.useState([]);
    ....

    ....
    const [pages, setPages]         = React.useState(1);
    const observer                  = React.useRef();

}

Now wrap up the API data in this Item component.

{items.map((item, index) =>
    (
        <Item key={index}>
            <div className="w-full md:w-3/5 bg-gray-300 mx-auto p-4 rounded mb-4 flex">
                <img src={item.thumbnailUrl} className="flex-auto mr-4" width="150" height="150" />
                <div className="flex-auto">
                    <h2 className="text-2xl font-semibold mb-2">{item.title}</h2>
                    <p className="text-sm">
                    Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset ...
                    </p>
                </div>
            </div>
        </Item>
    )
)}

The next step is to use the useCallback() hook and this hook will going to trigger on the last item.

{items.map((item, index) =>
    index + 1 === items.length ? (
    <Item reference={lastItemRef} key={index}>
        <div className="w-full md:w-3/5 bg-gray-300 mx-auto p-4 rounded mb-4 flex">
            <img src={item.thumbnailUrl} alt={`Image ${index}`} className="flex-auto mr-4" width="150" height="150" />
            <div className="flex-auto">
                <h2 className="text-2xl font-semibold mb-2">{item.title}</h2>
                <p className="text-sm">
                Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset ...
                </p>
            </div>
        </div>
    </Item>
    ) : (
    <Item key={index}>
        <div className="w-full md:w-3/5 bg-gray-300 mx-auto p-4 rounded mb-4 flex">
            <img src={item.thumbnailUrl} className="flex-auto mr-4" width="150" height="150" />
            <div className="flex-auto">
                <h2 className="text-2xl font-semibold mb-2">{item.title}</h2>
                <p className="text-sm">
                Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset ...
                </p>
            </div>
        </div>
    </Item>
    )
)}

We used the ternary condition to check and render the last item and assign the callback using reference property.

Let’s create a function using useCallback() hook.

const lastItemRef = React.useCallback(
    (node) => {
        if (isLoading) return;
        if (observer.current) observer.current.disconnect();
    
        observer.current = new IntersectionObserver((entries) => {
            if (entries[0].isIntersecting && hasMore) {
                if (pages < TOTAL_PAGES) {
                    getItems(pages);
                    setPages((pages) => pages + 1);
                } else {
                    setHasMore(false);
                }
            }
        });
    
        if (node) observer.current.observe(node);
    },
    [isLoading, hasMore]
);

The logic is pretty clear under the Intersection Observer call, the pages ( number of pages ) should be lesser than TOTAL_PAGES to fetch the data.

We hope this article will help you to learn how to use implement infinite scroll in React. If you like this article then please follow us on Facebook and Twitter.