useIntersectionObserver
Cuándo usarlo
Sección titulada «Cuándo usarlo»- Infinite scroll: un elemento “centinela” al final de la lista dispara cargar más cuando entra en el viewport.
- Lazy loading de imágenes o componentes pesados: montar/cargar solo cuando el elemento está por aparecer.
- Animar algo cuando entra en pantalla (fade-in, contador que arranca), sin un listener de
scrollcalculando posiciones a mano.
IntersectionObserver es la API del navegador pensada exactamente para esto — es más eficiente que un listener de scroll con getBoundingClientRect() en cada evento, porque el navegador calcula la intersección de forma nativa y asíncrona, sin bloquear el hilo principal.
import { type RefObject, useEffect, useRef, useState } from "react"
interface Options extends IntersectionObserverInit { /** Si es true, deja de observar apenas se vuelve visible una vez (útil para lazy load) */ once?: boolean}
export function useIntersectionObserver<T extends HTMLElement>( options: Options = {}): [RefObject<T | null>, boolean] { const { once = false, ...observerOptions } = options const ref = useRef<T>(null) const [isVisible, setIsVisible] = useState(false)
useEffect(() => { const el = ref.current if (!el) return
const observer = new IntersectionObserver(([entry]) => { setIsVisible(entry.isIntersecting)
if (entry.isIntersecting && once) { observer.disconnect() } }, observerOptions)
observer.observe(el) return () => observer.disconnect() // eslint-disable-next-line react-hooks/exhaustive-deps }, [ once, observerOptions.threshold, observerOptions.root, observerOptions.rootMargin ])
return [ref, isVisible]}// Lazy load: cargar la imagen real solo cuando está por entrar en pantallafunction LazyImage({ src, alt }: { src: string; alt: string }) { const [ref, isVisible] = useIntersectionObserver<HTMLDivElement>({ rootMargin: "200px", // empieza a cargar 200px antes de que sea visible once: true })
return ( <div ref={ref}> {isVisible && ( <img src={src} alt={alt} /> )} </div> )}
// Infinite scroll: centinela al final de la listafunction ListaConScroll({ onLoadMore }: { onLoadMore: () => void }) { const [sentinelRef, isVisible] = useIntersectionObserver<HTMLDivElement>()
useEffect(() => { if (isVisible) onLoadMore() }, [isVisible, onLoadMore])
return <div ref={sentinelRef} />}Umbrales, desconexión y accesibilidad
Sección titulada «Umbrales, desconexión y accesibilidad»rootMargines el ajuste más útil para lazy loading: un valor positivo ('200px') hace queisIntersectingse vuelvatrueantes de que el elemento sea literalmente visible, dando tiempo a que la imagen o los datos carguen mientras el usuario todavía está scrolleando hacia ahí.once: truedesconecta el observer cuando la condición se cumple por primera vez. Para carga diferida no tiene sentido seguir observando un recurso ya cargado; para animaciones que deben repetirse al entrar y salir, déjalo enfalse.- Las dependencias del efecto son deliberadamente explícitas (
threshold,root,rootMargin, no el objetoobserverOptionscompleto): un objeto literal pasado inline ({ threshold: 0.5 }) es una referencia nueva en cada render, así que depender del objeto entero re-crearía el observer todo el tiempo. Comparar sus campos primitivos evita ese loop. - Si necesitas varios thresholds o lógica más fina (por ejemplo, un porcentaje exacto de visibilidad),
entry.intersectionRatioda ese dato — el hook solo expone el booleano porque cubre el caso de uso más común, pero es fácil de extender devolviendoentrycompleto en vez de soloisVisible.