DEV Community

Cover image for Infinite scrolling using IntersectionObserver in React
Ahmad Shoaib Momenzada
Ahmad Shoaib Momenzada

Posted on

Infinite scrolling using IntersectionObserver in React

Have you ever felt that the information about implementing infinite scrolling in React is often confusing or incomplete? You're not alone. In this blog post, I'll be your guide in understanding infinite scrolling for React. We'll unravel the concept and make your React projects scroll seamlessly to infinity!

What's Intersection Observer?

According to the MDN docs, '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.' In simpler terms, the Intersection Observer API allows you to keep an eye on whether a specific element on a web page is in the view of the user or not. It's like having a virtual lookout that can alert your code when an element enters or exits the user's visible screen or a specified container. This can be incredibly useful for triggering actions or animations precisely when they become visible, enhancing the user experience in web development.

Implementation

Enough talk; let's dive into coding and implementing this. You might be wondering, how do we even create an Intersection Observer? Well, it's done by simply creating an instance of the Intersection Observer object, which is a built-in JavaScript API.

const observer = new IntersectionObserver(callback, options)
Enter fullscreen mode Exit fullscreen mode

The Intersection Observer requires two arguments:

  1. The callback is a function that will be executed whenever the observed element enters or exits the viewport.
  2. options is an optional configuration object that allows you to customize its behavior.

First, let's talk about the options:

const options = {
      root: null,
      rootMargin: "0px",
      threshold: 0.8,
    };
Enter fullscreen mode Exit fullscreen mode

root: The element that acts as the viewport for visibility checks. Defaults to the browser viewport.

rootMargin: Margin around the root, similar to CSS margins. Defaults to no margin.

threshold: A number or array indicating when the observer's callback should run based on the target's visibility. Defaults to running as soon as one pixel is visible; 0.5 means when 50% visible, and [0, 0.25, 0.5, 0.75, 1] specifies every 25% change.

The callback function is triggered when the viewport intersects with the target element. This means that the callback function is invoked when the target element becomes visible within the viewport or within the specified container.

The callback receives a list of entries, with each entry corresponding to a target that has reported a change in its intersection status. To determine if an entry represents an element currently intersecting with the root, you can check the value of the isIntersecting property.


const callback = (entries, observer) => { 
  entries.forEach(entry => {
       if (!entry.isIntersecting) {
          return;
        }
      FetchMoreData();
   });

Enter fullscreen mode Exit fullscreen mode

In this code snippet, we are checking whether the element is intersecting or not. If it is not intersecting, we simply do nothing and return. However, if it is intersecting, we proceed to fetch more data from that function.

React Implementation

Now that we have a good understanding of how the Intersection Observer works, let's proceed to implement it in React. One of the best ways to do this is by creating a custom hook, which we'll call useScrollToFetchData. Inside this hook, we will use useEffect to trigger the Intersection Observer every time an intersection event occurs. Additionally, we'll utilize the useRef hook to bind to an element, which will serve as the viewport for visibility checks.

export const useScrollToFetchData = (FetchMoreData:() => void) =>
{
  const bottom = useRef(null);

  useEffect(() => {
    const bottomCurrent = bottom.current;
    const options = {
      root: null,
      rootMargin: "0px",
      threshold: 0.8,
    };
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (!entry.isIntersecting) {
          return;
        }
        FetchMoreData();
      });
    }, options);

    if (bottomCurrent) {
      observer.observe(bottomCurrent);
    }

    return () => {
      if (bottomCurrent) {
        observer.unobserve(bottomCurrent);
      }
    };
  }, [FetchMoreData]);

  return bottom;
}
Enter fullscreen mode Exit fullscreen mode

In this code snippet, we use both the observe and unobserve methods on the IntersectionObserver object. When the element comes into the viewport, we start observing it with the observe method. Simultaneously, in the cleanup function of the useEffect, we call unobserve to stop observing the element when it goes out of the viewport.

As I mentioned at the beginning, many blogs and tutorials you find are incomplete. Here, we will further expand on the implementation of infinite scroll in React. When we fetch more data inside the Intersection Observer, there's a chance and possibility that the data will be fetched more than once, causing redundancy. To prevent this, there is a simple solution: using debounce from lodash.

const debouncedFetchMoreData = debounce(FetchMoreData, 2000);
Enter fullscreen mode Exit fullscreen mode

It's as simple as this: we pass the fetchMoreData function into debounce, and then inside the Observer, we can use debouncedFetchMoreData instead of fetchMoreData. Debounce essentially ensures that the fetchMoreData function is only called after a specified delay, preventing multiple rapid calls. In other words, debounce helps to control the rate at which a function is executed, preventing excessive or redundant calls.

Extra

Lastly, I'd like to mention two additional points when it comes to implementing infinite scrolling in React, especially for fetching more data.
As a best practice, ensure that the viewport element is always positioned at the end of the page. To achieve this, you can add specific CSS properties to control its placement.


<div ref={bottom} className="bottom-0 left-0 w-full flex justify-center h-1" />
Yes! Tailwind!
Enter fullscreen mode Exit fullscreen mode

Adding a loading spinner when more data is fetched and including a message to indicate that there is no more data to fetch is a common practice. While fetching more data, there will most likely be a state or context variable that keeps track of the fetched data, the ongoing data fetching process, and whether all the data has been fetched. By simply checking these variables, we can decide whether to display the loading spinner or the 'no more data' message.


 {!MoreData? (
    <div className="flex justify-center">
       <p className="text-lg text-center">No more Data</p>
    </div>
  ) : (
    <div className="flex justify-center">
       <img src={loadingSpinner} alt="Loading Spinner" />
    </div>
  )}

Enter fullscreen mode Exit fullscreen mode

In closing

Thank you for reading. I hope this has been helpful in terms of understanding IntersectionObserver and infinite scrolling.

Top comments (2)

Collapse
 
rajaerobinson profile image
Rajae Robinson

Great article!

I usually use a custom hook to abstract the logic for the Intersection Observer:

const useIntersectionObserver = (options, margin = '0px') => {
  const [isIntersecting, setIsIntersecting] = useState(false);
  const targetRef = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        setIsIntersecting(entry.isIntersecting);
      });
    }, {
      ...options,
      rootMargin: margin,
    });

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

    return () => {
      if (targetRef.current) {
        observer.unobserve(targetRef.current);
      }
    };
  }, [options, margin]);

  return { targetRef, isIntersecting };
};

export default useIntersectionObserver;
Enter fullscreen mode Exit fullscreen mode
Collapse
 
codeashdev profile image
Ahmad Shoaib Momenzada

On the spot implementation to completely separate the logic of intersectionObserver from the rest of the code.