Next.js
Intersection
Observer
Creating an Intersection Observer Component in Next.js
Introduction
When building a Next.js website, you may often encounter situations where you want to trigger specific events when a certain element becomes visible in the viewport.
This can include animations, lazy loading images, infinite scrolling, and much more. The Intersection Observer API makes this task a breeze.
In this tutorial, we will create a reusable component using this API, with a custom hook that fires when the observed element reaches the top or bottom of the viewport.
- Performance: Intersection Observer API runs asynchronously and is highly performant compared to traditional ways of listening for scroll events.
- Readability: By creating a custom hook and separating the logic from the component, the code is more maintainable and easier to understand.
- Reusability: The custom hook can be reused in other components, making your code DRY (Don't Repeat Yourself).
The Custom Hook
We'll start by creating a custom hook named 'useElementBoundaryObserver'. This hook will accept two parameters: 'rootmargin' and 'thresholdValue'. The 'rootMargin' parameter is used to grow or shrink each side of the root element's bounding box before computing intersections, and 'thresholdValue' represents at what percentage of the target's visibility the observer's callback should be executed.
import { useState, useEffect, useRef } from "react";
export function useElementBoundaryObserver(rootmargin, thresholdValue) {
const ref = useRef(null); // We initialize a useRef to track our element.
const [boundary, setBoundary] = useState(''); // The boundary state will indicate whether the element is at the top or bottom of the viewport.
useEffect(() => {
const currentRef = ref.current;
const observerOptions = {
root: null, // root null means it's relative to the viewport.
rootMargin: rootmargin,
threshold: thresholdValue,
};
// Create a new IntersectionObserver instance.
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
const rect = entry.boundingClientRect; // This provides the location of the observed element.
// Here we check the position of the element and update the boundary state accordingly.
if (rect.y <= 0) {
entry.isIntersecting ? setBoundary('topIn'):setBoundary('topOut');
} else if (rect.y <= (window.innerHeight || document.documentElement.clientHeight)) {
setBoundary('bottomIn');
} else if (rect.y >= (window.innerHeight || document.documentElement.clientHeight)) {
setBoundary('bottomOut');
}
});
}, observerOptions);
// Start observing the current reference.
if (currentRef) {
observer.observe(currentRef);
}
// Make sure to unobserve the element on component unmount to avoid memory leaks.
return () => {
if (currentRef) {
observer.unobserve(currentRef);
}
};
}, [rootmargin, thresholdValue]); // We add rootmargin and thresholdValue to the dependency array so the effect reruns when they change.
return [ref, boundary]; // Return the ref and boundary state.
}
The Component
Now let's create the 'NextIntersectionObserver' component. This component receives several properties: 'rootmargin', 'thresholdValue', 'classes', 'topIn', 'topOut', 'bottomIn', 'bottomOut'. Based on the 'boundary' state returned from our custom hook, we will modify the className of the element.
'use client';
import { useEffect, useState } from "react";
import { useElementBoundaryObserver } from "@/lib/customHooks";
export default function NextIntersectionObserver({ children, rootmargin, thresholdValue, classes, topIn, topOut, bottomIn, bottomOut }) {
const [ref, boundary] = useElementBoundaryObserver(rootmargin, thresholdValue);
const [className, setClassName] = useState(classes);
useEffect(() => {
// Update the className based on the boundary state.
switch (boundary) {
case 'topIn':
setClassName(`${classes} ${topIn}`);
break;
case 'topOut':
setClassName(`${classes} ${topOut}`);
break;
case 'bottomIn':
setClassName(`${classes} ${bottomIn}`);
break;
case 'bottomOut':
setClassName(`${classes} ${bottomOut}`);
break;
default:
setClassName(`${classes} ${bottomOut}`);
break;
}
}, [boundary, classes, topIn, topOut, bottomIn, bottomOut]);
return (
<div ref={ref} className={className}>
{children}
</div>
);
}
The 'useEffect' hook will fire whenever 'boundary' or any of the class variables changes. The advantage of this approach is that the class string is only re-computed when needed, saving unnecessary computations and potential re-renders.
Example Usage
Finally, let's see how to use this component in your Next.js application. Let's assume you have different CSS classes defined for 'topIn', 'topOut', 'bottomIn', 'bottomOut'.
import NextIntersectionObserver from '@/components/NextIntersectionObserver';
export default function Home() {
return (
<div>
<NextIntersectionObserver
rootmargin="0px"
thresholdValue={1.0}
classes="my-element"
topIn="top-in"
topOut="top-out"
bottomIn="bottom-in"
bottomOut="bottom-out"
>
Content goes here
</NextIntersectionObserver>
</div>
);
}
In the above code, when the element reaches the top of the viewport, it will have the classes 'my-element top-in'.As it goes out of the top of the viewport, it will change to 'my-element top-out'.
Similarly, when it enters from the bottom, the classes will be ''my-element bottom-in, and when it leaves from the bottom, they will change to 'my-element bottom-out'. This will allow you to apply different styles or animations depending on whether the element is currently visible in the viewport or not, and whether it's entering from the top or the bottom.
Conclusion
This approach to handling element visibility and triggering actions based on the element's location in the viewport is very powerful. It opens up a whole new realm of possibilities for animations, loading content, tracking user behavior, and much more.
The Intersection Observer API combined with the use of custom React Hooks, makes the code reusable, maintainable and enhances performance. Remember that the Intersection Observer runs asynchronously and is particularly suited to dealing with large amounts of elements, as opposed to using traditional methods like listening for scroll events.
Thanks to the reusability of the 'NextIntersectionObserver' component, you can now handle element boundary observation anywhere in your Next.js application, helping you to manage the visibility of any element with full control over how and when it interacts with the user's viewport.
With this, we wrap up our tutorial on creating an intersection observer component in Next.js. Now it's your turn to implement it and start playing around with this powerful feature. Happy coding!