Using Intersection Observer API in React

Using Intersection Observer API in React

ยท

4 min read

This article is about using the Intersection Observer API to enhance the visual experience of web applications. We will see the usage of this API in both HTML/CSS projects and React applications.

What is Intersection Observer API? (with a working example)

The Intersection Observer API provides us with a way to take some action asynchronously whenever a particular element that we are observing comes or goes out from the viewport / or a specified container. Let's understand it using an example:

//used in the codesandbox(Vanilla js) below:
const supportContainer = document.querySelector(".supporting");
const sectObserver = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      entry.target.classList.toggle("blockAnimation", entry.isIntersecting);
    });
  },
  {
    threshold: 0,
    rootMargin: "-100px"
  }
);

sectObserver.observe(supportContainer);

Let's break this down:

  • We create a new Intersection Observer using new IntersectionObserver(callback,options) syntax. This callback is triggered when it initially attaches to the DOM element and each time the element intersects based on the specified options.

  • Threshold value: A threshold of value 0 means that when any part of the observed element becomes visible, the isIntersecting property changes to true.

  • RootMargin: rootMargin is used to set the margins of the root property. In our case, root is the viewport. We are using the rootMargin property of -100px because of the footer height.

  • Callback: we pass a callback to the observer which toggles a blockAnimation class on our element based on the boolean value of isIntersecting property.

Codesandbox example using HTML, CSS, and Javascript:

Notice the slide-in effect of the supporting container in the codesandbox preview.

Infinite scroll in React:

Infinite scrolling is a popular feature in modern web applications. It is generally implemented using the intersection observer API. This section explains how to implement this feature, showcasing an example.

Link: https://custom-infinite-scroll-react.netlify.app/

Idea:

The idea behind infinite scrolling is simple, let's understand it:

  • We always observe the last item in the DOM using Intersection Observer API.

  • Whenever the last item comes into the view (intersects), we call the API to fetch the next set of items.

  • We disconnect the observer from the previous last item and connect it to the latest last item.

Code:

const App: React.FunctionComponent = () => {
  const [pageNumber, setPageNumber] = useState(1);
  const [todos, setTodos] = useState<todo[]>([]);
  const [loading, setLoading] = useState(false);
  const observer = useRef<IntersectionObserver>();

  const lastTodo = useCallback((node: Element | null) => {
    console.log(node);
    if (node != null) { // to check if the node exists
      if (observer.current) observer.current.disconnect();
      observer.current = new IntersectionObserver(
        (entries) => {
          const firstEntry = entries[0];
          if (firstEntry.isIntersecting) {
            setPageNumber((n) => n + 1);
          }
        },
        {
          threshold: 1,
        }
      );
      observer.current.observe(node);
    }
  }, []);

//this useEffect runs whenever the pageNumber gets changed
  useEffect(() => {
    fetchTodos(pageNumber, setTodos, setLoading);//fetches todos based on pageNumber
  }, [pageNumber]);

//JSX
  return (
    <div className="app">
      <header className="header">
        <h1>Custom Infinite Scroll</h1>
      </header>
      <main className="container">
        {todos.length
          ? todos.map((todo, index) => (
              <p
                className="todo"
                key={todo.id}
                ref={index === todos.length - 1 ? lastTodo : null}
              >
                {todo.title}
              </p>
            ))
          : ""}
        <div>{loading ? "Loading..." : ""}</div>
      </main>
      <footer className="footer">
        <p>Intersection Observer API</p>
      </footer>
    </div>
  );
};

export default App;

Let's break this down:

We declare three state variables:

  1. pageNumber: to increase the page number when reaching the end of the page.

  2. todos: an array of todos.

  3. loading: to indicate the loading state.

We initialize an observer using useRef and lastTodo using useCallback.

We can pass either a useRef reference to the ref attribute or a function to the ref attribute (callback ref). In our case, we are using the latter.

useCallback is a better option in this case because the callback that we pass to useCallback gets called every time it attaches itself to a DOM element. Specifically, when the component using this callback as a ref mounts, this callback gets called with the element as an argument. When this component unmounts, the callback gets called with null as an argument.

So, whenever the callback gets called, here's what happens:

  • We check if the node (element) is present or not; if it is, we proceed.

  • If an observer is already observing an element, we disconnect the observer.

  • We connect the observer to the new element.

When this element intersects our viewpoint, the pageNumber gets incremented. Whenever the pageNumber increases, the useEffect runs and fetches the next set of todos.

If you want to view the full code of the mentioned example: https://github.com/Hardiegogo/intersection-observer-mock

I hope you found this article helpful, see you in the next one!

Connect with me on Twitter | Linkedin | Github

Happy Coding! ๐ŸŽˆ๐ŸŽˆ

ย