Implementing a Reusable useIntersectionObserver React Hook

5 min read

📚 Quick Review: This practical application is built upon a fundamental programming concept. Review the Theory Lesson here first.


Building a Custom `useIntersectionObserver` React Hook

In this practical lesson, we’ll dive deep into the provided code snippet to understand how to construct a robust and reusable `useIntersectionObserver` React hook. This hook will allow any React component to easily detect when a specific DOM element enters or exits the viewport, enabling powerful features like lazy loading and scroll-triggered animations.

The Code:

import { useEffect, useState, useRef } from 'react';

export function useIntersectionObserver(options) {
  const [isIntersecting, setIsIntersecting] = useState(false);
  const targetRef = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      setIsIntersecting(entry.isIntersecting);
    }, options);

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

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

  return [targetRef, isIntersecting];
}

Line-by-Line Code Breakdown

Let’s dissect each part of this custom hook to understand its purpose and how it leverages React’s fundamental concepts and the browser’s Intersection Observer API.

`import { useEffect, useState, useRef } from ‘react’;`

This line imports the necessary React hooks:

  • `useState`: A hook that lets you add React state to function components. We’ll use it to store the intersection status.
  • `useEffect`: A hook that lets you perform side effects in function components. This is where we’ll set up and clean up our Intersection Observer.
  • `useRef`: A hook that returns a mutable ref object whose `.current` property is initialized to the passed argument. The returned object will persist for the full lifetime of the component. We’ll use it to hold a reference to the DOM element we want to observe.

`export function useIntersectionObserver(options) {`

This defines our custom hook. By convention, custom hooks start with `use`. It accepts an `options` object, which will be directly passed to the `IntersectionObserver` constructor, allowing customization of the observer’s behavior (e.g., `root`, `rootMargin`, `threshold`).

`const [isIntersecting, setIsIntersecting] = useState(false);`

Here, we initialize a piece of state called `isIntersecting`. This boolean will tell us whether our target element is currently visible within its root. It’s initialized to `false`, assuming the element is not visible initially. `setIsIntersecting` is the function to update this state.

`const targetRef = useRef(null);`

We create a `ref` named `targetRef`. This `ref` will be attached to the DOM element that we want to observe. Initially, its `.current` property is `null` because the DOM element hasn’t been rendered yet.

`useEffect(() => { … }, [options]);`

This is the core of our hook. The `useEffect` hook runs its callback function after every render where its dependencies have changed. The second argument, `[options]`, is the dependency array. This means the effect will re-run if the `options` object changes.

  • Observer Instantiation:
    const observer = new IntersectionObserver(([entry]) => {
      setIsIntersecting(entry.isIntersecting);
    }, options);

    Inside `useEffect`, we create a new `IntersectionObserver` instance. Its constructor takes two arguments: a callback function and an `options` object. The callback function is executed whenever the target element’s intersection status changes. It receives an array of `IntersectionObserverEntry` objects. We destructure the first `entry` from this array (assuming we’re observing a single element) and use `entry.isIntersecting` to update our `isIntersecting` state.

  • Observing the Target:
    if (targetRef.current) {
      observer.observe(targetRef.current);
    }

    After creating the observer, we check if `targetRef.current` exists. This ensures that the DOM element we want to observe has been rendered and assigned to the ref. If it exists, we call `observer.observe(targetRef.current)` to start observing that specific element.

  • Cleanup Function:
    return () => {
      if (targetRef.current) observer.unobserve(targetRef.current);
    };

    The `useEffect` hook allows us to return a cleanup function. This function runs when the component unmounts or before the effect re-runs (if dependencies change). Here, we ensure that if `targetRef.current` still exists, we call `observer.unobserve(targetRef.current)` to stop observing the element. This is crucial for preventing memory leaks and ensuring efficient resource management.

`return [targetRef, isIntersecting];`

Finally, our custom hook returns an array containing `targetRef` and `isIntersecting`. Components using this hook will destructure these values. They will attach `targetRef` to the DOM element they want to observe and use `isIntersecting` to conditionally render or animate content.

Execution Environment and Integration

This `useIntersectionObserver` hook is designed to run within a React component’s rendering lifecycle in a modern web browser environment.

How to use it in a React component:

To integrate this hook, you simply import it and use it within your functional components:

import React from 'react';
import { useIntersectionObserver } from './useIntersectionObserver'; // Adjust path as needed

function MyLazyLoadedComponent() {
  const [ref, isVisible] = useIntersectionObserver({
    threshold: 0.1 // Trigger when 10% of the element is visible
  });

  return (
    <div ref={ref} style={{ height: '300px', border: '1px solid gray', margin: '20px 0' }}>
      {isVisible ? (
        <p>I am visible! Content loaded.</p>
      ) : (
        <p>Scroll down to see me...</p>
      )}
    </div>
  );
}

export default MyLazyLoadedComponent;

In this example, the `div` element will have `ref` attached to it. When 10% of this `div` becomes visible in the viewport, `isVisible` will turn `true`, and the content inside will be rendered.

Browser Compatibility:

The Intersection Observer API is widely supported in modern browsers (Chrome, Firefox, Edge, Safari, Opera). For older browsers that do not support it, a polyfill might be necessary if you need to support them. Libraries like `intersection-observer` (a W3C polyfill) can be used for this purpose. However, for most modern web applications, native support is sufficient. The hook itself is pure JavaScript and React, making it highly portable.

💡 Developer Tip: Be mindful of the `options` object’s stability. If `options` is an object literal created directly inside the `useEffect`’s dependency array, it will be a new object on every render, causing the effect to re-run and the observer to be re-created unnecessarily. To prevent this, either memoize the `options` object using `useMemo` or define it outside the component if it’s static. For instance, `const options = useMemo(() => ({ threshold: 0.1 }), []);`

Leave a Reply

Your email address will not be published. Required fields are marked *