import { useEffect, useState, useRef } from 'react'
interface UseInViewOptions {
threshold?: number | number[]
rootMargin?: string
triggerOnce?: boolean
}
export function useInView(options: UseInViewOptions = {}) {
const { threshold = 0, rootMargin = '0px', triggerOnce = false } = options
const [inView, setInView] = useState(false)
const [hasTriggered, setHasTriggered] = useState(false)
const ref = useRef<HTMLElement>(null)
useEffect(() => {
const element = ref.current
if (!element || (triggerOnce && hasTriggered)) return
const observer = new IntersectionObserver(
([entry]) => {
const isInView = entry.isIntersecting
setInView(isInView)
if (isInView && triggerOnce) {
setHasTriggered(true)
}
},
{ threshold, rootMargin }
)
observer.observe(element)
return () => observer.disconnect()
}, [threshold, rootMargin, triggerOnce, hasTriggered])
return { ref, inView }
}
import { useInView } from '@/hooks/useInView'
interface LazyImageProps {
src: string
alt: string
className?: string
}
export function LazyImage({ src, alt, className }: LazyImageProps) {
const { ref, inView } = useInView({
threshold: 0,
rootMargin: '200px', // Start loading 200px before visible
triggerOnce: true,
})
return (
<img
ref={ref as any}
src={inView ? src : undefined}
alt={alt}
className={className}
loading="lazy"
/>
)
}
The Intersection Observer API efficiently detects when elements enter/exit the viewport, enabling lazy loading, infinite scroll, and analytics without expensive scroll listeners. I create observers with thresholds defining when callbacks trigger—0.0 means any pixel visible, 1.0 means fully visible. Observers watch multiple elements simultaneously with minimal performance cost. For lazy images, I load when they're about to enter viewport with a root margin buffer. Analytics track which content users actually view. Infinite scroll triggers fetches when sentinel elements become visible. This modern API replaces polling and improves performance significantly. All major browsers support it.