useIntersection
Tracks whether a target element has ever entered the intersection of the viewport (or specified root). Uses the Intersection Observer API .
const [setIntersection, hasIntersected, resetHasIntersected] = useIntersection<HTMLElement>({
rootRef: null, // optional, the ref of the ancestor element, defaults to the viewport
rootMargin: '200px', // optional
disabled: false // optional, skip observing entirely
});The returned boolean is named hasIntersected on purpose. It is sticky:
- It starts as
false. - It flips to
truethe first time the element enters the root (plusrootMargin). - It stays
trueforever after that, even once the element scrolls back out of view.
The hook stops observing the element as soon as it becomes true, so it will never flip back to false on its own. This design is deliberate. It targets the two most common cases where “did this ever become visible?” is the only question that matters:
- Lazy loading — once an image, an iframe, or a chunk of content has been loaded, it should stay loaded. Unloading it when it scrolls away would only cause it to be re-fetched later.
- Animate-on-scroll, once — a fade-in or slide-in should play the first time the element enters the viewport, and should not replay every time the user scrolls past it again.
Example
Here is an example of a <Thumbnail /> component that uses the useIntersection to lazy load the image.
import { useState, useCallback } from 'react';
import { useIntersection } from 'foxact/use-intersection';
// A foxact hook that can be used to reset the state when the props changes
// see: https://foxact.skk.moe/use-component-will-receive-update
import { useComponentWillReceiveUpdate } from 'foxact/use-component-will-receive-update';
interface ThumbnailProps {
src: string;
isLazy?: boolean;
}
const Thumbnail = ({ src, isLazy }: ThumbnailProps) => {
const [setIntersection, hasIntersected, resetHasIntersected] = useIntersection<HTMLImageElement>({
rootRef: null, // optional, the ref of the ancestor element
rootMargin: '200px', // optional
disabled: !isLazy // optional, allows to create reusable thumbnail component that supports both lazy and eager loading
});
// `hasIntersected` is sticky and never resets on its own, so reset it manually
// when the `src` prop changes and a different image needs to be lazy loaded.
// You can find the docs about `useComponentWillReceiveUpdate` here: https://foxact.skk.moe/use-component-will-receive-update
useComponentWillReceiveUpdate(resetHasIntersected, [src])
return (
<img
// Use callback ref to tell the `useIntersection` hook which image element needs to be observed
// Wraps the callback ref with `useCallback` to avoid unnecessary invocations during re-render.
// See also react documentation about callback ref: https://react.dev/reference/react-d../../components/common#ref-callback
ref={useCallback((el: HTMLImageElement | null) => {
if (isLazy) {
setIntersection(el);
}
}, [isLazy, setIntersection])}
decoding="async"
alt={alt}
// Once the image has entered the viewport once, `hasIntersected` stays `true`,
// so the loaded image is never swapped back to the placeholder.
src={hasIntersected ? src : 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'}
/>
);
}And here is the animate-on-enter case, where the animation play only once on first entering the viewport:
import { useCallback } from 'react';
import { useIntersection } from 'foxact/use-intersection';
const FadeInSection = ({ children }: { children: React.ReactNode }) => {
const [setIntersection, hasIntersected] = useIntersection<HTMLDivElement>({
rootMargin: '-10% 0px'
});
return (
<div
ref={setIntersection}
// `hasIntersected` never goes back to `false`, so scrolling away and back
// will not replay the animation.
className={hasIntersected ? 'fade-in' : 'fade-in-initial'}
>
{children}
</div>
);
};Resetting
The third element of the returned tuple, resetHasIntersected, sets hasIntersected back to false and starts observing the element again. It is the only way the state can become false after it has become true.
Reset it when the element is reused for a different piece of content — e.g. the src of a lazy loaded image changes, or a list item is recycled for a different row. useComponentWillReceiveUpdate is a convenient way to do this during a prop change, as shown in the example above.