```tsx
export function useIntersectionObserver(
ref: React.RefObject<Element>,
options?: IntersectionObserverInit
) {
const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);
useEffect(() => {
if (!ref.current) return;
const observer = new IntersectionObserver(([e]) => setEntry(e), options);
observer.observe(ref.current);
return () => observer.disconnect();
}, [ref, JSON.stringify(options)]); // serialize options as dep proxy
return entry;
}
// Usage: lazy-load images
function LazyImage({ src, alt }: { src: string; alt: string }) {
const ref = useRef<HTMLImageElement>(null);
const entry = useIntersectionObserver(ref, { threshold: 0.1, rootMargin: "100px" });
const visible = entry?.isIntersecting ?? false;
return <img ref={ref} src={visible ? src : undefined} alt={alt} />;
}
```
Common uses:
- Infinite scroll (observe a sentinel at list end; when it's visible, load more).
- Lazy-load images / heavy components.
- Track viewport analytics ("user actually saw this content").
- Pause expensive offscreen work.
Threshold semantics: the callback fires when the visible ratio crosses any threshold. `threshold: [0, 0.25, 0.5, 0.75, 1]` fires 5 times during a smooth scroll. Use `threshold: 0` for "any pixel visible" toggling. Use `rootMargin: "100px"` to trigger 100px *before* visibility for prefetching.
The full reference solution in every supported language stays in the editor above - reveal it there once you have had a real attempt.