DEV Community

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

Posted on • Originally published at phuoc.ng

Create a reusable component for IntersectionObserver

In our previous post, we learned how to create a custom hook that simplifies the IntersectionObserver API logic. Now, let's explore another approach to reuse this implementation, but in a different way.

As React developers, we know how important it is to create reusable components. It reduces code duplication, improves maintainability, and enhances performance. By creating reusable components, we can easily use them throughout our application and across different projects, ensuring consistency in design and functionality.

Moreover, creating a reusable component for a specific task or feature enables us to update it quickly and efficiently if any changes or improvements are necessary. This saves time and effort compared to having to modify the same code in multiple places.

In this post, we'll create a reusable component for IntersectionObserver using a design pattern called Function as a Child (FAAC). But before we dive into the component implementation, let's explore what the FACC pattern is all about.

Introducing the Function as a Child pattern in React

When working with children props in React, you'll typically come across components or simple strings/numbers. But did you know that the children prop can also be a function?

Enter the Function as a Child (FAAC) pattern. This popular technique in React allows for more dynamic communication between parent and child components. Instead of directly passing down props, the parent component passes down a function as a child. The child component can then call this function with any necessary data, providing more flexibility and interactivity between components.

This pattern is especially useful when the child component needs to interact with its parent in some way. By passing a function as a child, the parent retains control over what data is being passed down, while still allowing the child to manipulate and use that data in a meaningful way.

The Function as a Child pattern is a widely used technique in many libraries and frameworks, including React Router, a popular routing library for React applications. This pattern allows the router to pass its routing information down to its child components, giving them access to the router's state and enabling them to navigate between different routes.

Here is a simple code snippet showing how to use React Router:

<Router>
    <nav>
        <ul>
            <li><Link to="/">Home</Link></li>
            <li><Link to="/about">About</Link></li>
            <li><Link to="/topics">Topics</Link></li>
        </ul>
    </nav>

    <Switch>
        {/* Render some UI here based on the current URL */}
        <Route exact path="/">
        {({ match }) => (
            // Render some UI here if the current URL matches "/"
        )}
        </Route>

        {/* Render some UI here based on the ID parameter */}
        <Route path="/topics/:topicId">
        {({ match }) => (
            // Render some UI here if the current URL matches "/topics/:topicId"
        )}
        </Route>

        {/* Render some UI here for any other URLs */}
        <Route path="/">
        {({ match }) => (
            // Render some UI here if no other routes match
        )}
        </Route>
    </Switch>
</Router>
Enter fullscreen mode Exit fullscreen mode

Here's an example of how React Router passes routing information to its child components using a "Function as a Child". The Route component takes a function as its child, which receives an object containing information on whether the current route matches the specified path. This allows you to conditionally render UI based on the current URL.

Simplifying the IntersectionObserver API with a custom component

In the previous post, we discussed how the custom hook returns an array of two items - a React ref representing the element we want to watch for intersection changes, and a Boolean variable indicating whether the element is visible or not.

Now, we can simplify things even further by turning that custom hook into a component, using the Function as a Child pattern.

Here's a draft of the component implementation:

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

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

    return children({
        ref,
        isVisible,
    });
};
Enter fullscreen mode Exit fullscreen mode

Our IntersectionWatcher component uses the FAAC pattern to make a highly customizable and reusable IntersectionObserver component. We pass a function as a child that returns an object with two properties: ref and isVisible.

The ref property is a callback that helps us track changes to a specific DOM element. By attaching the ref to the target element through the ref attribute, the callback function will update our internal node state. At first, the node state is set to null.

The isVisible property is a Boolean value indicating whether the observed element is currently visible in the viewport.

To encapsulate the IntersectionObserver API, we continue to use the useEffect hook, just like we did in the custom hook. Here's a quick reminder of the code snippet we used earlier:

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

        return () => {
            observer.unobserve(node);
        };
    }, [node]);
};
Enter fullscreen mode Exit fullscreen mode

Using the custom component

Using the IntersectionWatcher component is easy. Simply pass a function as a child that receives an object with two properties: ref and isVisible.

The ref property is a React callback ref that captures the DOM element we want to observe for intersection changes. The isVisible property is a Boolean value that tells us whether or not the observed element is currently visible in the viewport.

<IntersectionWatcher>
{
    ({ ref, isVisible }) => (
        /* Render the target element */
    )
}
</IntersectionWatcher>
Enter fullscreen mode Exit fullscreen mode

In this function, we have the power to render any JSX elements we desire. By using the ref property, we can easily attach the intersection observer to any element that we wish to monitor for intersection changes via the ref attribute.

({ ref, isVisible }) => (
    <div className="element" ref={ref}>...</div>
)
Enter fullscreen mode Exit fullscreen mode

We can use the isVisible property to show or hide UI elements based on whether or not they are currently visible in the viewport.

Here's an example:

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

Check out the demo below. Just scroll up and down to see the message update automatically, telling you whether the target element is partially visible or not.

Taking component customization further

Our component can now detect whether an element is partially visible in the viewport. But what if we want to know if it's fully visible, as 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, we can make some changes to our IntersectionWatcher component. First, we'll add a new property called threshold. This property will take an array of numbers between 0 and 1, which will specify the percentage of the target's visibility at which the observer's callback should be executed.

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

Here's the updated version of the IntersectionWatcher component:

export const IntersectionWatcher = ({ children, threshold }) => {
    React.useEffect(() => {
        const observer = new IntersectionObserver(([entry]) => {
            setIsVisible(entry.isIntersecting);
        }, { threshold });
        // ...
    }, [node]);
};
Enter fullscreen mode Exit fullscreen mode

By default, the threshold value is set to [0], which means that the callback function is called as soon as the target element intersects with the root element.

But, if you pass an array of values between 0 and 1 as the threshold prop when using IntersectionWatcher, you can customize when the callback function should be called based on how much of the target element is visible in the viewport. For example, a threshold value of [0.5] would call the callback function when at least 50% of the target element is visible.

Here's an example of how to use IntersectionWatcher with a custom threshold value:

<IntersectionWatcher threshold={[0.5]}>
{({ ref, isVisible }) => (
    <div ref={ref}>
        {isVisible ? 'At least 50% visible' : 'Less than 50% visible'}
    </div>
)}
</IntersectionWatcher>
Enter fullscreen mode Exit fullscreen mode

To determine if an element is fully visible on the screen, we can set a threshold value of [1]. This ensures that the element is completely visible before taking any action.

By allowing for custom threshold values, our IntersectionObserver component becomes even more versatile and useful across a variety of use cases.

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

Conclusion

To wrap it up, the IntersectionObserver API is a handy tool that lets us know when a target element intersects with another element or the viewport. If we encapsulate this functionality in a custom hook or component, we can create solutions that are highly reusable and customizable for different scenarios.

With the help of React's hooks and Function as a child pattern, we can create components that are flexible and easy to use. Our IntersectionWatcher component is a good example of how these patterns can be used to build powerful and reusable code.

See also


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

Top comments (0)