"Is this element on screen?" powers a huge amount of UI: lazy-loading images as they scroll into view, infinite-scroll sentinels, fade-in-on-reveal animations, viewability analytics. The efficient way to answer it is the IntersectionObserver API — the browser watches an element against a viewport and calls you back when its visibility crosses a threshold, with none of the scroll-listener jank. useIntersectionObserver wraps that API into a hook.
Implement useIntersectionObserver(elementRef, options). Observe elementRef.current, keep the latest IntersectionObserverEntry in state, and return it (so callers read entry?.isIntersecting). Support threshold, root, rootMargin, and a freezeOnceVisible flag that stops observing after the element first appears.
function useIntersectionObserver(elementRef, {
threshold = 0, root = null, rootMargin = '0%', freezeOnceVisible = false,
}) {
// returns the latest IntersectionObserverEntry (or undefined)
}
const ref = useRef(null);
const entry = useIntersectionObserver(ref, { threshold: 0.5 });
const isVisible = entry?.isIntersecting;
<div ref={ref}>{isVisible ? <Chart /> : <Placeholder />}</div>
// One-shot reveal: freeze so it never flips back to hidden.
const entry = useIntersectionObserver(ref, { freezeOnceVisible: true });
new IntersectionObserver(cb, options), observe(node), and disconnect() in the cleanup.elementRef.current is null or IntersectionObserver isn't available (older browsers / SSR).freezeOnceVisible — once isIntersecting is true, stop observing and keep the last entry — ideal for lazy-load and reveal-once.