[Digging into the Observer API] 2. IntersectionObserver
[Digging into the Observer API] 2. IntersectionObserver
IntersectionObserver is an observer specialized for handling the DOM and the viewport. By detecting whether a DOM element intersects with the viewport, it can distinguish whether that element is actually visible on the user's screen or hidden.
TYPESCRIPT
const io = new IntersectionObserver(callback, options);
You can use it with the code above, which takes two parameters: callback and options.
IntersectionObserver's callback parameter is an event method that runs when an element becomes visible or invisible in the viewport.
It's declared with the type IntersectionObserverCallback, and expressed in code, looks like this.
TYPESCRIPT
const io = new IntersectionObserver((entries, observer) => {});
entries has the type IntersectionObserverEntry[]. It returns event information for elements registered with the IntersectionObserver, in the form of an array.
The reason it's an array type is that multiple DOM elements can be registered with a single IntersectionObserver.
🖼️ isIntersecting
The isIntersecting property returns intersection status as a boolean value. Depending on the specified options, it returns true if the DOM element is visible, and false otherwise.
🖼️ target
Similar to the commonly seen target property in event objects, this returns the element on which the event occurred.
🖼️ intersectionRatio
Returns the ratio by which the DOM element intersects, as a number. This is closely tied to the threshold option, described later.
If you specify multiple ratios by passing an array to threshold, this value lets you identify which specific ratio was reached.
🖼️ rootBounds
Returns the DOM element that serves as the viewport's reference, as an Element. If the root option wasn't specified in options, it returns null.
Returns the time the event occurred, as a number.
🖼️ time
Returns bounding box information for the target DOM element. You can get x, y, width, and height information.
🖼️ intersectionRect
Returns bounding box information for the intersecting area between the target DOM element and the viewport. Like boundingClientRect, you get x, y, width, and height information, but with the difference that it's limited to the area intersecting the viewport.
Returns the IntersectionObserver object. This lets you chain operations on the IntersectionObserver even from within the callback method.
You can control IntersectionObserver's detailed behavior by passing the options property.
🖼️ root
You can specify the root DOM element that serves as the reference for the viewport. If not specified, the currently visible browser viewport is used.
🖼️ rootMargin
Intersection events occur relative to the root DOM element, and rootMargin lets you adjust the detection area.
For example, if root is the viewport and rootMargin is set to 100px, the intersection event fires starting from 100px beyond the viewport.
This lets you fire events ahead of time in a preemptive fashion.
Just like CSS's margin property, you can also specify it as y x or top right bottom left.
🖼️ threshold
Specifies the intersection ratio required for the intersection event to fire, as a number or number[], with each element being a real number between 0 and 1. For example, 0.2 fires the intersection event when the intersection ratio reaches 20%.
If specified as an array, the event fires at each specified ratio. That is, with [ 0.2, 0.5, 0.8 ], an event fires separately at 20%, 50%, and 80% intersection.
When using IntersectionObserver, you'll find that registering a DOM element at the right point in the lifecycle is more cumbersome than you'd expect.
Let's make IntersectionObserver easier to use with a custom hook.
TYPESCRIPT
export function useIntersectionObserver(): void { // }
Let's start by defining a method called useIntersectionObserver as above.
To use useIntersectionObserver, we need the following three elements.
- Target DOM element
- Callback method
- Options
Let's define these three elements as parameters. For item 1, an HTMLElement is normally required, but for this hook, we'll design it so it accepts not only the HTMLElement type but also a string, so a tag can be targeted via a selector like #id or .class.
TYPESCRIPT
import { useEffect } from "react"; export type UseIntersectionObserverCallback = ( entry: IntersectionObserverEntry ) => void; /** * IntersectionObserver hook method * * @param {Element | string | null} ref: Element * @param {UseIntersectionObserverCallback} callback: callback method * @param {IntersectionObserverInit} options: options */ export function useIntersectionObserver( ref: Element | string | null, callback: UseIntersectionObserverCallback, options?: IntersectionObserverInit ): void { // }
Here's the parameter definition. Next, let's declare IntersectionObserver and assign the callback method and options.
TYPESCRIPT
import { useEffect } from "react"; export type UseIntersectionObserverCallback = ( entry: IntersectionObserverEntry ) => void; /** * IntersectionObserver hook method * * @param {Element | string | null} ref: Element * @param {UseIntersectionObserverCallback} callback: callback method * @param {IntersectionObserverInit} options: options */ export function useIntersectionObserver( ref: Element | string | null, callback: UseIntersectionObserverCallback, options?: IntersectionObserverInit ): void { useEffect(() => { const io = new IntersectionObserver((entries) => { entries.forEach(callback); }, options); }, [ref, callback, options]); }
As shown above, initialize IntersectionObserver and assign it to the io variable.
Next, assign the DOM element specified by ref.
ref goes through the following branching, depending on which of the three states it's in.
- If null -> skip.
- If string -> select the tag with document.querySelector, then register that tag.
- If HTMLElement -> since it's already an Element, register it immediately.
This is implemented by checking whether ref is a string via typeof ref === "string".
You can register a tag via the io.observe() method.
TYPESCRIPT
import { useEffect } from "react"; export type UseIntersectionObserverCallback = ( entry: IntersectionObserverEntry ) => void; /** * IntersectionObserver hook method * * @param {Element | string | null} ref: Element * @param {UseIntersectionObserverCallback} callback: callback method * @param {IntersectionObserverInit} options: options */ export function useIntersectionObserver( ref: Element | string | null, callback: UseIntersectionObserverCallback, options?: IntersectionObserverInit ): void { useEffect(() => { const io = new IntersectionObserver((entries) => { entries.forEach(callback); }, options); // If the DOM element is valid if (ref) { // If ref is a string if (typeof ref === "string") { const tag = document.querySelector(ref); // If the tag is valid if (tag) { io.observe(tag); } } // If it's a DOM element else { io.observe(ref); } } }, [ref, callback, options]); }
Finally, add cleanup code so IntersectionObserver registrations don't pile up every time the component re-renders.
You can remove IntersectionObserver via the io.disconnect() method.
TYPESCRIPT
import { useEffect } from "react"; export type UseIntersectionObserverCallback = ( entry: IntersectionObserverEntry ) => void; /** * IntersectionObserver hook method * * @param {Element | string | null} ref: Element * @param {UseIntersectionObserverCallback} callback: callback method * @param {IntersectionObserverInit} options: options */ export function useIntersectionObserver( ref: Element | string | null, callback: UseIntersectionObserverCallback, options?: IntersectionObserverInit ): void { useEffect(() => { const io = new IntersectionObserver((entries) => { entries.forEach(callback); }, options); // If the DOM element is valid if (ref) { // If ref is a string if (typeof ref === "string") { const tag = document.querySelector(ref); // If the tag is valid if (tag) { io.observe(tag); } } // If it's a DOM element else { io.observe(ref); } } return () => { io.disconnect(); }; }, [ref, callback, options]); }
That's the full code.
I implemented a simple example on CodeSandbox. Possibly due to how CodeSandbox handles rendering, rootMargin doesn't seem to work properly there.
Using IntersectionObserver like this makes it easy to handle a DOM element's viewport-related aspects.
This API makes it easy to build features that are complicated to implement with the traditional Event Driven approach, such as infinite scroll or triggering animations.
Let's cover ResizeObserver in the next post.