Core APIs
Choose and use the three React APIs for observing elements.
react-intersection-observer exports two hooks and one component. They take the same observer options and differ only in how they hand you the result.
| API | Use it when | What it returns |
|---|---|---|
useInView |
Visibility affects rendered output. | A callback ref, inView, and the latest entry. |
useOnInView |
You need an effect without a hook-owned state update. | A callback ref and your (inView, entry) callback. |
<InView> |
Render props or a wrapper component fit the composition. | Render-prop fields, or a generated wrapper for plain children. |
There is also a low-level observe function for code that already owns a DOM element.
Common configuration choices
- Load or reveal once with
triggerOnce. - Start work before the target reaches the viewport with
rootMargin. - Require a meaningful amount of content to be visible with
threshold. - Observe inside a scrolling container with
root. - Use
useOnInViewfor analytics or prefetching that should not create a hook-owned re-render.
useInView
Use useInView when visibility belongs in React state:
import { useInView } from "react-intersection-observer";
export function ArticleSection() {
const { ref, inView } = useInView();
return <section ref={ref}>{inView ? "In view" : "Waiting"}</section>;
}
The hook supports both object and tuple destructuring:
const { ref, inView, entry } = useInView();
const [ref, inView, entry] = useInView();
entry is undefined until the first accepted notification. After that it is the latest IntersectionObserverEntry, so read intersectionRatio, boundingClientRect, or target from it when the UI needs geometry.
onChange
useInView can call onChange alongside its state update:
const { ref, inView } = useInView({
onChange(nextInView, entry) {
console.log(entry.target, nextInView);
},
});
Use useOnInView instead when the callback is the only result you need.
useOnInView
useOnInView returns a ref callback and never updates component state. Use it for analytics, logging, prefetching, and anything else that should not cause a render:
import { useOnInView } from "react-intersection-observer";
export function TrackedCard({ id }: { id: string }) {
const ref = useOnInView(
(inView, entry) => {
if (inView) {
analytics.track("card_visible", { id, target: entry.target });
}
},
{ threshold: 0.5, triggerOnce: true },
);
return <article ref={ref}>Card {id}</article>;
}
The callback receives (inView, entry). useOnInView accepts the options that affect observation: root, rootMargin, scrollMargin, threshold, triggerOnce, skip, trackVisibility, and delay. It does not accept onChange, initialInView, or fallbackInView.
<InView>
Use render props when the component should receive the ref and visibility state directly:
import { InView } from "react-intersection-observer";
export function RevealCard() {
return (
<InView threshold={0.2} triggerOnce>
{({ ref, inView, entry }) => (
<article ref={ref} className={inView ? "card visible" : "card"}>
<h2>Card</h2>
<p>{entry ? "The observer has reported" : "Waiting"}</p>
</article>
)}
</InView>
);
}
The render prop receives { ref, inView, entry }. Attach the ref to the element you want to observe.
Plain children
Plain children always render. In this form <InView> creates a wrapper element, forwards any extra HTML props to it, and observes it:
<InView as="section" className="article" onChange={handleChange}>
<h2>Always-rendered content</h2>
</InView>
Use as to keep the generated wrapper semantic. This form does not forward refs. Use render props or useInView when you need direct control of the observed element, or a ref on a custom component.
Low-level observe
When you already own a DOM element outside React rendering, use the low-level observe function. Keep the cleanup function it returns:
import { observe } from "react-intersection-observer";
const stop = observe(
element,
(inView, entry) => {
console.log(inView, entry.intersectionRatio);
},
{ threshold: 0.5 },
);
// Call this when the element is no longer relevant.
stop();
Shared behavior
triggerOncestops observing after the first acceptedtruetransition.skipdisables observation while preserving the current state.- When an observed node is removed and later replaced, the hook resets to
initialInView, unlesstriggerOnceorskipprevents the reset. entryisundefinedbefore the first accepted notification, and again after an observed node resets.
See Configuration for every option and Testing for deterministic observer transitions.