DEV Community

Cover image for Create a reusable hook for IntersectionObserver
Phuoc Nguyen
Phuoc Nguyen

Posted on • Originally published at phuoc.ng

Create a reusable hook for IntersectionObserver

In our previous post, we learned how to use the useEffect hook to integrate IntersectionObserver with React. But what if we want to use this implementation in other places? It's always a good idea to turn it into a reusable function that we can use in different components and projects.

Creating a reusable hook can save us a lot of time and effort. Instead of writing the same code multiple times, we can abstract it into a single function that can be used across different projects. This not only saves us time but also makes our code more organized and easier to maintain.

Moreover, creating a reusable hook allows us to share our code with the community. We can publish our hooks as NPM packages, which other developers can use in their own projects. This promotes code reuse and helps to build a stronger React ecosystem.

In this post, we'll learn how to create a custom hook that encapsulates the IntersectionObserver API logic. Let's dive in!

Simplifying the IntersectionObserver API with a custom hook

In the previous post, we introduced an implementation that uses a React ref to represent an element and a Boolean state to indicate whether the element is partially visible or not.

To make this implementation reusable, we created a useIntersectionObserver hook that encapsulates the IntersectionObserver logic. This custom hook returns an array containing two elements: ref and isVisible.

The ref element is a callback function that receives the DOM node as its argument. We attach this ref to the component's root element, which we want to observe for intersection. Once the ref is attached to the target element via the ref attribute, the callback will be executed to update our internal node state. Initially, the node state is set to null.

The second element returned by this hook is a Boolean value indicating if the observed element is currently intersecting with the viewport.

Here's how we can draft the hook implementation.

export const useIntersectionObserver = () => {
    const [node, setNode] = React.useState<HTMLElement>(null);
    const [isVisible, setIsVisible] = React.useState(false);

    const ref = React.useCallback((nodeEle) => {
        setNode(nodeEle);
    }, []);

    return [ref, isVisible];
};
Enter fullscreen mode Exit fullscreen mode

To track changes in a node's intersection, we can use the useEffect() hook. This hook has two arguments: a callback function and an array of dependencies. In our case, we only need to run the effect when the node element exists, so we pass [node] as the second argument.

Inside the callback function, we first check if the node exists. If it does, we create a new instance of IntersectionObserver and pass it a callback that gets called every time there is an intersection change. The callback receives an array of IntersectionObserverEntry objects, but since we are only observing one element, we can safely assume that this array will always have only one element.

We then update our internal state with the current value of entry.isIntersecting, which tells us whether or not the observed element is currently intersecting with the viewport.

Finally, we return a cleanup function that stops observing the node when it's no longer needed. This ensures that we don't waste resources by observing elements that are no longer in use.

Here's the updated version of the hook for your reference:

export const useIntersectionObserver = () => {
    React.useEffect(() => {
        if (!node) {
            return;
        }
        const observer = new IntersectionObserver(([entry]) => {
            setIsVisible(entry.isIntersecting);
        });
        observer.observe(node);

        return () => {
            observer.unobserve(node);
        };
    }, [node]);

    return [ref, isVisible];
};
Enter fullscreen mode Exit fullscreen mode

Using the custom hook

To use the useIntersectionObserver hook, we start by importing it into our component. Once imported, we simply call the hook and destructure its return values into variables.

import { useIntersectionObserver } from './useIntersectionObserver';

const [elementRef, isVisible] = useIntersectionObserver();
Enter fullscreen mode Exit fullscreen mode

The first variable is a ref callback function that we pass to the element we want to observe for intersection. This is done using the ref attribute in JSX.

// Render
<div className="element" ref={elementRef}>
    ...
</div>
Enter fullscreen mode Exit fullscreen mode

The second variable is a simple true or false value that tells us if the element is currently visible on the screen or not. We can use this value to show or hide content or take other actions based on whether or not an element is visible to the user.

// Render
<div className="result">
    {isVisible ? 'Element is partially visible' : 'Element is not partially visible'}
</div>
Enter fullscreen mode Exit fullscreen mode

Take a look at the demo below. Simply scroll up and down to see the message update automatically, informing you whether or not the target element is partially visible.

Customizing the hook further

Now that our hook can detect whether an element is partially visible in the viewport, what if we want to know if it's fully visible, like we did before? In our previous post, we achieved this by setting the threshold value to an array of 1.

const observer = new IntersectionObserver(([entry]) => {
    setIsVisible(entry.isIntersecting);
}, {
    threshold: [1],
});
Enter fullscreen mode Exit fullscreen mode

To support the threshold value, let's modify our useIntersectionObserver hook. We can add a new parameter called threshold that takes an array of numbers between 0 and 1. This array specifies the percentage of the target's visibility at which the observer's callback should be executed.

Next, we pass this threshold value as an option to the IntersectionObserver constructor. In the updated implementation, we pass a second argument to IntersectionObserver with an options object containing our threshold value.

export const useIntersectionObserver = ({
    threshold,
}: {
    threshold: number[],
}) => {
    React.useEffect(() => {
        const observer = new IntersectionObserver(([entry]) => {
            setIsVisible(entry.isIntersecting);
        }, {
            threshold,
        });

        // ...
    }, [node]);
};
Enter fullscreen mode Exit fullscreen mode

Now, when we call our custom hook, we can pass in a threshold value as an argument. For instance, if we want to find out if an element is completely visible on the screen, we can set a threshold value of [1]. This will help us ensure that our element is fully visible before taking any action.

const [elementRef, isVisible] = useIntersectionObserver({
    threshold: [1],
});
Enter fullscreen mode Exit fullscreen mode

Our custom hook just got even more flexible and reusable! We added support for the threshold parameter, making it suitable for various intersection detection scenarios.

Give it a go by scrolling up and down in the playground below to see how this updated implementation works.

Conclusion

In conclusion, we have learned how to make a reusable hook for IntersectionObserver in React. By putting the IntersectionObserver logic in a custom hook, we can easily use it across different components and projects. We also learned how to make this hook even more customizable by adding support for the threshold parameter.

Creating custom hooks is just one of the many ways we can make our code more modular and easier to maintain. It allows us to take complex logic and turn it into reusable building blocks that can be shared with other developers.

See also


If you want more helpful content like this, feel free to follow me:

Top comments (0)