useIntersectionObserver
Description
useIntersectionObserver is a hook for observing a DOM element’s visibility via the native IntersectionObserver. It subscribes to the element from ref, can invoke a callback on each change, and exposes a lazily‑activated currentEntry field with the observer’s latest record.
The hook returns a hybrid structure: you can destructure it as a tuple ([currentEntry]) or as an object ({ currentEntry }).
Signature
function useIntersectionObserver<ElementType extends Element | null>(
ref: RefObject<ElementType | null>,
callback?: UseIntersectionObserverCallback,
): UseIntersectionObserverReturn;Parameters
ref— a ref object to the element whose visibility should be tracked.callback?— a function called on each change; receives the currentIntersectionObserverEntry.
Returns:
UseIntersectionObserverReturn- A hybrid object/tuple with
currentEntry: IntersectionObserverEntry | null— the latest observer record (updated as changes occur; re-renders are enabled after the first external access to this field).
- A hybrid object/tuple with
Examples
1) Check whether an element is in view
import { useRef, useEffect } from 'react';
import { useIntersectionObserver } from '@webeach/react-hooks';
export function Section() {
const ref = useRef<HTMLDivElement>(null);
const { currentEntry } = useIntersectionObserver(ref);
useEffect(() => {
if (currentEntry?.isIntersecting) {
console.log('Element is visible on screen');
}
}, [currentEntry]);
return (
<div ref={ref} style={{ height: 300 }}>
Section
</div>
);
}2) Side effects via callback only
import { useRef } from 'react';
import { useIntersectionObserver } from '@webeach/react-hooks';
export function AnalyticsTracker() {
const ref = useRef<HTMLElement>(null);
useIntersectionObserver(ref, (entry) => {
if (entry.isIntersecting) {
console.log('Send analytics event: element appeared');
}
});
return <section ref={ref} style={{ minHeight: 400 }} />;
}3) Lazy-loading images
import { useRef, useEffect, useState } from 'react';
import { useIntersectionObserver } from '@webeach/react-hooks';
export function LazyImage({ src, alt }: { src: string; alt: string }) {
const ref = useRef<HTMLImageElement>(null);
const { currentEntry } = useIntersectionObserver(ref);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
if (currentEntry?.isIntersecting) {
setLoaded(true);
}
}, [currentEntry]);
return <img ref={ref} src={loaded ? src : undefined} alt={alt} />;
}Behavior
Subscription & resubscription
- The observer attaches to
ref.currentafter mount and re-attaches when the element changes.
- The observer attaches to
Lazy reactivity for
currentEntry- Re-renders for
currentEntryare enabled on the first external read. Until it is read, updates do not trigger re-renders.
- Re-renders for
Callback is independent of
currentEntry- If
callbackis provided, it runs on every intersection change regardless of whethercurrentEntryis used.
- If
Cleanup
- The observer is automatically disconnected on unmount and when the target element changes.
SSR-safe
- Subscription logic runs only in the browser; no side effects execute during server rendering.
When to Use
Lazy loading
- Load images, video, or data only when the element enters the viewport.
Scroll-triggered animations
- Start animations when a block appears on screen.
Analytics
- Track section/banner views without global scroll listeners.
When Not to Use
One-off visibility check
- For a single position calculation,
getBoundingClientRectis sufficient.
- For a single position calculation,
If only window scrolling matters
- Prefer global events (
scroll,resize) instead of a per-element observer.
- Prefer global events (
Common Mistakes
refnot attached to an element- If
ref.current === null, no subscription happens. Ensure the ref is passed to the intended element.
- If
Expecting re-renders without reading
currentEntry- If you rely only on
callback, the component won’t re-render — this is by design.
- If you rely only on
Accidental resubscription
- Keep the
refstable; don’t create a new ref on each render.
- Keep the
Types
Exported types
UseIntersectionObserverCallback(entry: IntersectionObserverEntry) => void.
UseIntersectionObserverReturn- Hybrid: object
{ currentEntry: IntersectionObserverEntry | null }and tuple[currentEntry: IntersectionObserverEntry | null].
- Hybrid: object
UseIntersectionObserverReturnObject- Object form:
{ currentEntry: IntersectionObserverEntry | null }.
- Object form:
UseIntersectionObserverReturnTuple- Tuple form:
[currentEntry: IntersectionObserverEntry | null].
- Tuple form: