How to implement Infinite Scroll using React?

How to implement Infinite Scroll using React?

Hello everyone, I hope you all are doing well. Recently I learned about the infinite scroll and implemented it in one of my projects. Since then, many of my friends have asked me how to implement it. Today, I'm going to share my learnings with you and make it easy for you to implement infinite scroll in your projects.

I will be showing you the step-by-step process and what each line does So that even a beginner can understand the process. This article will help you understand:

  • What is IntersectionObserver?
  • How can we use IntersectionObserver to implement Infinite Scroll
  • How can you create your own custom infinite scroll

I will be using ReactJS. But, you can also use vanilla JS or any other framework because the concept remains the same irrespective of which framework you are using.

Before going into the implementation, We will need an API from which we can request data by providing its page and limit. It will be something similar to: https://xyz.app/api/products/1/10 Where 1 is for page number. That is the current index of which part of products we want from our API. 10 is for the limit, no of items we want in one request

Don't worry. If you are working in a company, you probably won't be creating this API, A backend developer will provide it, and we'll just be calling it on our frontend. For this blog, I've created a basic API on replit, which you can use for practice and understanding the concept.

Here's the code If you are curious about the implementation of API: replit.com/@shobit1337/infinite-scroll#inde..

We'll be using this API to fetch data on scroll.

API URL: https://infinite-scroll.shobit1337.repl.co/products/:limit/:page

Let's first understand the starter files, that we'll be modifying to implement an infinite scroll. In this sandbox, I am loading products that have an image, name, and price that are coming from the API. On the page load, I've made a GET call to https://infinite-scroll.shobit1337.repl.co/products and stored the response to our products state. which will be rendered on the JSX. This is a straightforward approach to loading any data to our page on the first load.

You can play around with this sandbox and build it with me, side by side. Just fork this and start coding along, Or you can refer to the final sandbox that you can find at the end of the blog.

Starter Files: stackblitz.com/edit/react-bkxnct

What is IntersectionObserver?

IntersectionObserver is a web API provided to us by browsers, using this we can perform changes depending upon the visibility of an HTML element in our viewport.

Use Case: We can perform any changes or call a function whenever an element is visible on the screen. This can be used to perform Lazy loading of images, Infinite scroll, To check How much content a user has viewed on your website, etc

In this article we are just gonna discuss Infinite scroll, It will give you all a clear idea So that you can build your own implementation.

So, the First thing, we need to define is a LIMIT that will say, How many products to load whenever we hit the bottom. For our example, I've defined it as 5. You can define it anything you want But keep in mind that this API only has a limit of 100 products max.

The next thing we require is a state to maintain the pageNo we are on, This will be used to fetch data from API that will take pageNo and limit to return response.

const LIMIT = 5;
const [pageNo, setPageNo] = useState(1);

Whenever this pageNo state will change, We want to trigger an effect that will again fetch our API with updated data, and update our products state. Let's make the changes to our useEffect So, It will call whenever the pageNo state changes

  useEffect(() => {
    // Fetching Products from API whenever pageNo value updates
      (async () => {
        const { data } = await axios.get(
          `https://infinite-scroll.shobit1337.repl.co/products/${LIMIT}/${pageNo }`
        );
        // Appending new data to the already present data
        setProducts((previous) => [...previous, ...data.list]);
      })();
  }, [pageNo ]);

As you can see in the above code snippet, I have also replaced the API URL that now used LIMIT and pageNo, This API call will give us data for that particular page only, and then in the next line, we are appending the data.list to our original products state.

Our API part is done, Now we just need to figure out how to update the pageNo state, So, that It will fire our useEffect and automatically fetch more data and add to our products.

One Simple Solution is to just add a button at the end of our list, LOAD MORE, and add a onClick function to it. That will increment the pageNo value and more products will be loaded. But we don't want that, we need to implement Infinite scroll that will keep on automatically load more products whenever will reaches the end of the current list. This is where IntersectionObserver comes into the picture.

Before IntersectionObserver, we had to add an onscroll eventListener on our page that was more complex and harder to implement.

To use IntersectionObserver we need to do two things

  • Creating a new instance of the Intersection Observer Object with a callback function
  • Registering our element that will be observed continuously by our IntersectionObserver.

Since, we need to create this new instance whenever our page loads, we will be using useEffect hook to initialize our IntersectionObserver on page load.

Let's take a look at the code and understand the working one by one.

  useEffect(() => {
    // #1 Initialize
    const observer = new IntersectionObserver(handleObserver);

    // #2 Register our observer with an element.
    if (elementRef) observer.observe(elementRef);

    // #3 Clean up when our component unmount, Remove the observer
    return () => {
      observer.unobserve(elementRef);
    };
  }, []);

#1, we are creating a new instance of IntersectionObserver which is taking a callback function ( We'll see its implementation next ) that will be called automatically every time our element comes in the visible area of screen/viewport.

#2, Our observer doesn't know which element to watch for, So, we'll also need to add the element reference that will be continuously observed. This observer Object has an access to observer() function. which takes element reference as an argument.

#3, Finally, When the component unmount, we will need to remove our observer, For that we use unobserve(). It will stop executing the overserver callback for that element that we pass into unobserve.

Let's see how to define elementRef and handleObserver:

Let's create an element that will act as a trigger point for our observer and point to its reference using a useRef() hook;

const loader = useRef(null);
const elementRef = loader.current;

Add this element at the end of your list.

<div ref={loader} />

Now, we are done with the element part, we just need an handleObserverfunction that we are passing into our new IntersectionObserver(handleObserver),

    const handleObserver = (entries) => {
      const target = entries[0];
      if (target.isIntersecting) {
        setPageNo((prev) => ++prev);
      }
    };

handleObserver gets an entries array that will contain all the objects of elements that we are observing. In our case, we have one element registered, so we take entries[0] value. This target object has some props like, isIntersecting, intersectionRatio , boundingClientRect. that we can use to implement our logic. we just need to check if our target element isIntersecting ( It will check if our targeted element is in the viewport of our screen ), If it returns true and pageNo will be incremented by one. That will trigger our useEffect and more products will be fetched and added to our state.

Combining it together

After combining everthing, This is how our Logic will looks like:

const LIMIT = 5;
const [product, setProduct] = useState([]);
const [pageNo, setPageNo] = useState(1);
const loader = useRef(null);

// Registering our Element to be observed continiously
useEffect(() => {
  const elementRef = loader.current;
  const handleObserver = (entries) => {
    const target = entries[0];
    if (target.isIntersecting) {
      setPageNo((prev) => ++prev);
    }
  };
  const observer = new IntersectionObserver(handleObserver);
  if (elementRef){
    observer.observe(elementRef);
  }
  return () => {
    observer.unobserve(elementRef);
  };
}, []);

// Making call to API whenever pageNo changes and update our products state
useEffect(() => {
  (async () => {
    const { data } = await axios.get(
      `https://infinite-scroll.shobit1337.repl.co/products/${LIMIT}/${pageNo}`
    );
    // Appending new data to the already present product state
    setProduct((state) => [...state, ...data.list]);
  })();
}, [pageNo]);

and the JSX:

return (
   <>
      <div className="App">
        {product.map((item) => (
          <span className="items" key={item.id}>
            <img src={item.image} alt="product-img" />
            <span>{item.name}</span>
            <span>Price: {item.price}</span>
          </span>
        ))}
        <div ref={loader} />
      </div>
    </>
  );

And that's it! You've just implemented the infinite scroll feature. I hope that you liked the blog. Do let me know if you have any feedback. Also, I'd really love to know which topic you want me to write about next?

Final code sandbox: stackblitz.com/edit/react-zipxvr?ctl=1&..

PS: I have also added a loading state to make it more visually good. Do take a look, Let me know if you have any doubts left :)