← Back toIntersectionObserver with React

Create a reusable hook for IntersectionObserver

Written byPhuoc Nguyen
Created
23 Jan, 2024
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.
ts
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];
};
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:
ts
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];
};

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.
tsx
import { useIntersectionObserver } from './useIntersectionObserver';

const [elementRef, isVisible] = useIntersectionObserver();
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.
tsx
// Render
<div className="element" ref={elementRef}>
...
</div>
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.
tsx
// Render
<div className="result">
{isVisible ? 'Element is partially visible' : 'Element is not partially visible'}
</div>
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.
tsx
const observer = new IntersectionObserver(([entry]) => {
setIsVisible(entry.isIntersecting);
}, {
threshold: [1],
});
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.
ts
export const useIntersectionObserver = ({
threshold,
}: {
threshold: number[],
}) => {
React.useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
setIsVisible(entry.isIntersecting);
}, {
threshold,
});

// ...
}, [node]);
};
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.
ts
const [elementRef, isVisible] = useIntersectionObserver({
threshold: [1],
});
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

Questions? 🙋

Do you have any questions about front-end development? If so, feel free to create a new issue on GitHub using the button below. I'm happy to help with any topic you'd like to learn more about, even beyond what's covered in this post.
While I have a long list of upcoming topics, I'm always eager to prioritize your questions and ideas for future content. Let's learn and grow together! Sharing knowledge is the best way to elevate ourselves 🥷.
Ask me questions

Recent posts ⚡

Newsletter 🔔

If you're into front-end technologies and you want to see more of the content I'm creating, then you might want to consider subscribing to my newsletter.
By subscribing, you'll be the first to know about new articles, products, and exclusive promotions.
Don't worry, I won't spam you. And if you ever change your mind, you can unsubscribe at any time.
Phước Nguyễn