Exploring Infinite Scroll Techniques in react
Tags: react, javascript, webdev, tutorial
Published: 2023-06-13 18:30:01

Infinite scroll has become a popular technique in web development to provide a seamless and dynamic user experience when dealing with large sets of data. It allows users to scroll through content endlessly without explicit pagination or loading new pages. In this blog, we will explore how to implement infinite scroll in a React application, leveraging its virtualization capabilities and optimizing performance.

There are 3 ways to implement infinite scrolling in react.

## 1. Using react libraries

To start implementing infinite scroll in your React application, we need to install some dependencies. The most commonly used library for this purpose is react-infinite-scroll-component. You can install it using npm or yarn,

`npm install react-infinite-scroll-component axios`
                     or
`yarn add react-infinite-scroll-component axios`

After that we need to import the components,

```javascript
import React, { useState, useEffect } from "react";
import InfiniteScroll from "react-infinite-scroll-component";
```
Set the initial states of our component. This includes a list of items, load flags, and variables that store the index of the next page. 

```javascript
import React, { useState, useEffect } from "react";
import InfiniteScroll from "react-infinite-scroll-component";
import axios from "axios";

const InfiniteScrollExample1 = () => {
  const [items, setItems] = useState([]);
  const [hasMore, setHasMore] = useState(true);
  const [index, setIndex] = useState(2);
  
  // Rest of the components

```
Now let's see how the data is fetched from the backend. Here we are using Axios library and [Platzi Fake Store](https://api.escuelajs.co/docs) for fetch dummy data.
 
So there are two parts in the code. First, using the `useEffect` hook, we retrieve the initial product set from the API and update the `items` state variable with the resolved API response's data.

The second part, `fetchMoreData` function is defined separately to handle fetching more data when the user reaches the end of the page or triggers a specific event.
When the new data comes back, it adds it to the existing products in the items variable. It also checks if there are more products left to load, and if so, it sets a variable called `hasMore` to `true`, so we know we can load more later.
And at the end of the function updates the `index` state.

```javascript
  useEffect(() => {
    axios
      .get("https://api.escuelajs.co/api/v1/products?offset=10&limit=12")
      .then((res) => setItems(res.data))
      .catch((err) => console.log(err));
  }, []);

  const fetchMoreData = () => {
    axios
      .get(`https://api.escuelajs.co/api/v1/products?offset=${index}0&limit=12`)
      .then((res) => {
        setItems((prevItems) => [...prevItems, ...res.data]);

        res.data.length > 0 ? setHasMore(true) : setHasMore(false);
      })
      .catch((err) => console.log(err));

    setIndex((prevIndex) => prevIndex + 1);
  };
```

Then wrap the list of items in the InfiniteScroll component. Configure the component by passing the necessary props like dataLength, next, hasMore, and loader

```javascript
 return (
    <InfiniteScroll
      dataLength={items.length}
      next={fetchMoreData}
      hasMore={hasMore}
      loader={<Loader />}
    >
      <div className='container'>
        <div className='row'>
          {items &&
            items.map((item) => <ProductCard data={item} key={item.id} />)}
        </div>
      </div>
    </InfiniteScroll>
  );
```
And so everything would be complete:

```javascript
import React, { useState, useEffect } from "react";
import InfiniteScroll from "react-infinite-scroll-component";
import axios from "axios";
import ProductCard from "./ProductCard";
import Loader from "./Loader";

const InfiniteScrollExample1 = () => {
  const [items, setItems] = useState([]);
  const [hasMore, setHasMore] = useState(true);
  const [index, setIndex] = useState(2);

  useEffect(() => {
    axios
      .get("https://api.escuelajs.co/api/v1/products?offset=10&limit=12")
      .then((res) => setItems(res.data))
      .catch((err) => console.log(err));
  }, []);

  const fetchMoreData = () => {
    axios
      .get(`https://api.escuelajs.co/api/v1/products?offset=${index}0&limit=12`)
      .then((res) => {
        setItems((prevItems) => [...prevItems, ...res.data]);

        res.data.length > 0 ? setHasMore(true) : setHasMore(false);
      })
      .catch((err) => console.log(err));

    setIndex((prevIndex) => prevIndex + 1);
  };

  return (
    <InfiniteScroll
      dataLength={items.length}
      next={fetchMoreData}
      hasMore={hasMore}
      loader={<Loader />}
    >
      <div className='container'>
        <div className='row'>
          {items &&
            items.map((item) => <ProductCard data={item} key={item.id} />)}
        </div>
      </div>
    </InfiniteScroll>
  );
};

export default InfiniteScrollExample1;

```
## 2. Building a Custom Solution
If you prefer a custom solution, you can implement infinite scroll by handling the scroll event manually. Let's see the code

```javascript
import React, { useState, useEffect, useCallback } from "react";
import axios from "axios";
import ProductCard from "./ProductCard";
import Loader from "./Loader";

const InfiniteScrollExample2 = () => {
  const [items, setItems] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [index, setIndex] = useState(2);

  // Rest of the component


```
We can define the fetchData function using the useCallback hook to handle data fetching

```javascript
  const fetchData = useCallback(async () => {
    if (isLoading) return;

    setIsLoading(true);

    axios
      .get(`https://api.escuelajs.co/api/v1/products?offset=${index}0&limit=12`)
      .then((res) => {
        setItems((prevItems) => [...prevItems, ...res.data]);
      })
      .catch((err) => console.log(err));
    setIndex((prevIndex) => prevIndex + 1);

    setIsLoading(false);
  }, [index, isLoading]);
```
We can fetch the initial data using the useEffect hook

```javascript
useEffect(() => {
    const getData = async () => {
      setIsLoading(true);
      try {
        const response = await axios.get(
          "https://api.escuelajs.co/api/v1/products?offset=10&limit=12"
        );
        setItems(response.data);
      } catch (error) {
        console.log(error);
      }
      setIsLoading(false);
    };

    getData();
  }, []);
```
Next, we handle the scroll event and call the fetchData function when the user reaches the end of the page

```javascript
  useEffect(() => {
    const handleScroll = () => {
      const { scrollTop, clientHeight, scrollHeight } =
        document.documentElement;
      if (scrollTop + clientHeight >= scrollHeight - 20) {
        fetchData();
      }
    };

    window.addEventListener("scroll", handleScroll);
    return () => {
      window.removeEventListener("scroll", handleScroll);
    };
  }, [fetchData]);
```
Finally, render the list of items along with a loader component

```javascript
return (
    <div className='container'>
      <div className='row'>
        {items.map((item) => (
          <ProductCard data={item} key={item.id} />
        ))}
      </div>
      {isLoading && <Loader />}
    </div>
  );
};
```
And so everything would be complete:

```javascript
import React, { useState, useEffect, useCallback } from "react";
import axios from "axios";
import ProductCard from "./ProductCard";
import Loader from "./Loader";

const InfiniteScrollExample2 = () => {
  const [items, setItems] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [index, setIndex] = useState(2);

  const fetchData = useCallback(async () => {
    if (isLoading) return;

    setIsLoading(true);

    axios
      .get(`https://api.escuelajs.co/api/v1/products?offset=${index}0&limit=12`)
      .then((res) => {
        setItems((prevItems) => [...prevItems, ...res.data]);
      })
      .catch((err) => console.log(err));
    setIndex((prevIndex) => prevIndex + 1);

    setIsLoading(false);
  }, [index, isLoading]);

  useEffect(() => {
    const getData = async () => {
      setIsLoading(true);
      try {
        const response = await axios.get(
          "https://api.escuelajs.co/api/v1/products?offset=10&limit=12"
        );
        setItems(response.data);
      } catch (error) {
        console.log(error);
      }
      setIsLoading(false);
    };

    getData();
  }, []);

  useEffect(() => {
    const handleScroll = () => {
      const { scrollTop, clientHeight, scrollHeight } =
        document.documentElement;
      if (scrollTop + clientHeight >= scrollHeight - 20) {
        fetchData();
      }
    };

    window.addEventListener("scroll", handleScroll);
    return () => {
      window.removeEventListener("scroll", handleScroll);
    };
  }, [fetchData]);

  return (
    <div className='container'>
      <div className='row'>
        {items.map((item) => (
          <ProductCard data={item} key={item.id} />
        ))}
      </div>
      {isLoading && <Loader />}
    </div>
  );
};

export default InfiniteScrollExample2;

```


## 3. Leveraging the Intersection Observer API
Another approach to implementing infinite scroll is by leveraging the Intersection Observer API. 
The Intersection Observer API is a modern development technique that can detect when elements appear, thus triggering content loading for infinite scrolling. 
The Intersection Observer API observes changes in the intersection of the target elements with the ancestor or view element, making it well suited for implementing infinite scroll. 
Let's see how to implement

```javascript
import React, { useState, useEffect, useRef, useCallback } from "react";
import axios from "axios";

const InfiniteScrollExample3 = () => {
  const [items, setItems] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [index, setIndex] = useState(2);
  const loaderRef = useRef(null);
  
   // Rest of the code

```
We can fetch the initial data using the `useEffect` hook

```javascript
useEffect(() => {
    const getData = async () => {
      setIsLoading(true);
      try {
        const response = await axios.get(
          "https://api.escuelajs.co/api/v1/products?offset=10&limit=12"
        );
        setItems(response.data);
      } catch (error) {
        console.log(error);
      }
      setIsLoading(false);
    };

    getData();
  }, []);
```

The `fetchData` function is a special type of function created with the `useCallback` hook. It remembers its definition and changes only if its dependencies (in this case `index` and `isLoading`) change. 
Its purpose is to handle additional data retrieval from the API when it is invoked.

```javascript
const fetchData = useCallback(async () => {
  if (isLoading) return;

  setIsLoading(true);
  axios
    .get(`https://api.escuelajs.co/api/v1/products?offset=${index}0&limit=12`)
    .then((res) => {
      setItems((prevItems) => [...prevItems, ...res.data]);
    })
    .catch((err) => console.log(err));

  setIndex((prevIndex) => prevIndex + 1);

  setIsLoading(false);
}, [index, isLoading]);
```
The `useEffect` hook is used to configure the intersection watcher, which monitors the visibility of the loading element in the viewport. When the loading item is displayed, indicating that the user has scrolled down, the `fetchData` function is invoked to fetch additional data.
The cleanup function ensures that the loader item is not observed when the component is no longer in use to avoid unnecessary observation. 

```javascript
  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      const target = entries[0];
      if (target.isIntersecting) {
        fetchData();
      }
    });

    if (loaderRef.current) {
      observer.observe(loaderRef.current);
    }

    return () => {
      if (loaderRef.current) {
        observer.unobserve(loaderRef.current);
      }
    };
  }, [fetchData]);
```
Finally, render the list of items along with a loader component

```javascript
  return (
    <div className='container'>
      <div className='row'>
        {items.map((item, index) => (
          <ProductCard data={item} key={item.id} />
        ))}
      </div>
      <div ref={loaderRef}>{isLoading && <Loader />}</div>
    </div>
  );
```
And so everything would be complete:

```javascript
import React, { useState, useEffect, useRef, useCallback } from "react";
import axios from "axios";
import ProductCard from "./ProductCard";
import Loader from "./Loader";

const InfiniteScrollExample3 = () => {
  const [items, setItems] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [index, setIndex] = useState(2);
  const loaderRef = useRef(null);

  const fetchData = useCallback(async () => {
    if (isLoading) return;

    setIsLoading(true);
    axios
      .get(`https://api.escuelajs.co/api/v1/products?offset=${index}0&limit=12`)
      .then((res) => {
        setItems((prevItems) => [...prevItems, ...res.data]);
      })
      .catch((err) => console.log(err));

    setIndex((prevIndex) => prevIndex + 1);

    setIsLoading(false);
  }, [index, isLoading]);

  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      const target = entries[0];
      if (target.isIntersecting) {
        fetchData();
      }
    });

    if (loaderRef.current) {
      observer.observe(loaderRef.current);
    }

    return () => {
      if (loaderRef.current) {
        observer.unobserve(loaderRef.current);
      }
    };
  }, [fetchData]);

  useEffect(() => {
    const getData = async () => {
      setIsLoading(true);
      try {
        const response = await axios.get(
          "https://api.escuelajs.co/api/v1/products?offset=10&limit=12"
        );
        setItems(response.data);
      } catch (error) {
        console.log(error);
      }
      setIsLoading(false);
    };

    getData();
  }, []);

  return (
    <div className='container'>
      <div className='row'>
        {items.map((item, index) => (
          <ProductCard data={item} key={item.id} />
        ))}
      </div>
      <div ref={loaderRef}>{isLoading && <Loader />}</div>
    </div>
  );
};

export default InfiniteScrollExample3;

```
And this is what it would look like.

![infinite scroll](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tral3joxhsuo0mvm8eoi.gif)

By exploring these two techniques, you can choose the one that best fits your project requirements and provide a smooth and engaging infinite scroll experience for your users.🎈

I hope you found this article enjoyable and insightful! If you liked it, please feel free to leave a comment and share your thoughts. Thank you!🥰

For a detailed reference and access to the complete source code, you can find the Git repository 👉 [here](https://github.com/vishnusatheeshpulickal/react-infinite-scroll/)



<a href="https://www.buymeacoffee.com/vishnusatheesh" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 30px!important;width: 150px!important;" ></a>