DEV Community

Cover image for Exploring Infinite Scroll Techniques in react
Vishnu Satheesh
Vishnu Satheesh

Posted on • Updated on

Exploring Infinite Scroll Techniques in react

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,

import React, { useState, useEffect } from "react";
import InfiniteScroll from "react-infinite-scroll-component";
Enter fullscreen mode Exit fullscreen mode

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.

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

Enter fullscreen mode Exit fullscreen mode

Now let's see how the data is fetched from the backend. Here we are using Axios library and Platzi Fake Store 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.

  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);
  };
Enter fullscreen mode Exit fullscreen mode

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

 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>
  );
Enter fullscreen mode Exit fullscreen mode

And so everything would be complete:

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;

Enter fullscreen mode Exit fullscreen mode

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

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


Enter fullscreen mode Exit fullscreen mode

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

  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]);
Enter fullscreen mode Exit fullscreen mode

We can fetch the initial data using the useEffect hook

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();
  }, []);
Enter fullscreen mode Exit fullscreen mode

Next, we handle the scroll event and call the fetchData function when the user reaches the end of the page

  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]);
Enter fullscreen mode Exit fullscreen mode

Finally, render the list of items along with a loader component

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

And so everything would be complete:

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;

Enter fullscreen mode Exit fullscreen mode

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

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

Enter fullscreen mode Exit fullscreen mode

We can fetch the initial data using the useEffect hook

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();
  }, []);
Enter fullscreen mode Exit fullscreen mode

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.

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]);
Enter fullscreen mode Exit fullscreen mode

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.

  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]);
Enter fullscreen mode Exit fullscreen mode

Finally, render the list of items along with a loader component

  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>
  );
Enter fullscreen mode Exit fullscreen mode

And so everything would be complete:

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;

Enter fullscreen mode Exit fullscreen mode

And this is what it would look like.

infinite scroll

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

Buy Me A Coffee

Top comments (3)

Collapse
 
pierre profile image
Pierre-Henry Soria ✨

This is a very well-explained and neat post! Thanks for sharing this Vishnu!

Collapse
 
vishnusatheesh profile image
Vishnu Satheesh

Thank you so muchπŸ₯°

Collapse
 
devgancode profile image
Ganesh Patil

React is awesome JS Library πŸ‘¨πŸ»β€πŸ«
Thanks for sharing Vishnu.
Learn React for free - shorturl.at/fEFIW