Create a reusable component for IntersectionObserver
Tags: webdev, javascript, react, typescript
Published: 2024-04-01 01:42:24

In our previous post, we learned how to create a [custom hook](https://phuoc.ng/collection/intersection-observer-with-react/create-a-reusable-hook-for-intersection-observer/) 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:

```tsx
<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>
```

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:

```tsx
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,
    });
};
```

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:

```tsx
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]);
};
```

## 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.

```tsx
<IntersectionWatcher>
{
    ({ ref, isVisible }) => (
        /* Render the target element */
    )
}
</IntersectionWatcher>
```

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.

```tsx
({ ref, isVisible }) => (
    <div className="element" ref={ref}>...</div>
)
```

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:

```tsx
({ ref, isVisible }) => (
    <div className="result">
        {isVisible ? 'Element is partially visible' : 'Element is not partially visible'}
    </div>
)
```

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.

{% codesandbox kg659t %}

## 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](https://phuoc.ng/collection/intersection-observer-with-react/use-the-intersection-observer-api-in-react/#checking-if-an-element-is-fully-visible), as we did before? In our previous post, we achieved this by setting the `threshold` value to an array of 1.

```tsx
const observer = new IntersectionObserver(([entry]) => {
    setIsVisible(entry.isIntersecting);
}, {
    threshold: [1],
});
```

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:

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

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:

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

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.

{% codesandbox mffqq5 %}

## 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

-   [Pass refs to child components using the function as a child pattern](https://phuoc.ng/collection/react-ref/pass-refs-to-child-components-using-the-function-as-a-child-pattern/)

---

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

-   [DEV](https://dev.to/phuocng)
-   [GitHub](https://github.com/phuocng)
-   [Website](https://phuoc.ng)