Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
import { useState, useEffect, useRef, useCallback } from "react";
interface UseIntersectionObserverOptions {
/** Elemento que se usa como ventana gráfica. Predeterminado: ventana gráfica del navegador */
root?: Element | null;
/** Margen alrededor de la raíz. Predeterminado: "0px" */
rootMargin?: string;
/** Porcentaje del elemento visible para disparar. 0-1 o array. Predeterminado: 0 */
threshold?: number | number[];
/** Solo disparar una vez (p. ej., para lazy loading). Predeterminado: false */
triggerOnce?: boolean;
/** Comenzar a observar inmediatamente. Predeterminado: true */
enabled?: boolean;
}
interface UseIntersectionObserverReturn {
/** Ref para adjuntar al elemento de destino */
ref: (node: Element | null) => void;
/** La última entrada de IntersectionObserverEntry */
entry: IntersectionObserverEntry | null;
/** Si el elemento está intersecando actualmente */
isIntersecting: boolean;
}
function useIntersectionObserver(
options: UseIntersectionObserverOptions = {}
): UseIntersectionObserverReturn {
const {
root = null,
rootMargin = "0px",
threshold = 0,
triggerOnce = false,
enabled = true,
} = options;
const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);
const observerRef = useRef<IntersectionObserver | null>(null);
const nodeRef = useRef<Element | null>(null);
const frozenRef = useRef(false);
const cleanup = useCallback(() => {
if (observerRef.current) {
observerRef.current.disconnect();
observerRef.current = null;
}
}, []);
// Patrón de callback ref para dirigirse flexiblemente a elementos
const ref = useCallback(
(node: Element | null) => {
// Limpiar observer anterior
cleanup();
nodeRef.current = node;
if (!node || !enabled || frozenRef.current) return;
if (typeof IntersectionObserver === "undefined") return;
observerRef.current = new IntersectionObserver(
([observedEntry]) => {
setEntry(observedEntry);
if (triggerOnce && observedEntry.isIntersecting) {
frozenRef.current = true;
cleanup();
}
},
{ root, rootMargin, threshold }
);
observerRef.current.observe(node);
},
[root, rootMargin, threshold, triggerOnce, enabled, cleanup]
);
// Limpieza al desmontar
useEffect(() => {
return cleanup;
}, [cleanup]);
return {
ref,
entry,
isIntersecting: entry?.isIntersecting ?? false,
};
}Cuándo usarlo: Necesitas imágenes cargadas perezosamente, disparadores de scroll infinito, efectos de animación al desplazarse o seguimiento de qué secciones ha desplazado el usuario.
"use client";
// Imagen cargada perezosamente
function LazyImage({ src, alt }: { src: string; alt: string }) {
const { ref, isIntersecting } = useIntersectionObserver({
triggerOnce: true,
rootMargin: "200px", // Comenzar a cargar 200px antes de ser visible
});
return (
<div ref={ref} style={{ minHeight: 200, background: "#f0f0f0" }}>
{isIntersecting ? (
<img src={src} alt={alt} style={{ width: "100%" }} />
) : (
<div style={{ padding: 20, color: "#999" }}>Cargando...</div>
)}
</div>
);
}
// Disparador de scroll infinito
function InfiniteList() {
const [items, setItems] = useState<number[]>([1, 2, 3, 4, 5]);
const [loading, setLoading] = useState(false);
const { ref, isIntersecting } = useIntersectionObserver({
threshold: 1.0,
});
useEffect(() => {
if (!isIntersecting || loading) return;
setLoading(true);
// Simular llamada a API
setTimeout(() => {
setItems((prev) => [
...prev,
...Array.from({ length: 5 }, (_, i) => prev.length + i + 1),
]);
setLoading(false);
}, 500);
}, [isIntersecting, loading]);
return (
<div>
{items.map((item) => (
<div key={item} style={{ padding: 24, borderBottom: "1px solid #eee" }}>
Elemento {item}
</div>
))}
<div ref={ref} style={{ padding: 20, textAlign: "center" }}>
{loading ? "Cargando más..." : "Desplázate para más"}
</div>
</div>
);
}
// Animar al desplazarse
function AnimatedSection({ children }: { children: React.ReactNode }) {
const { ref, isIntersecting } = useIntersectionObserver({
threshold: 0.2,
triggerOnce: true,
});
return (
<div
ref={ref}
style={{
opacity: isIntersecting ? 1 : 0,
transform: isIntersecting ? "translateY(0)" : "translateY(20px)",
transition: "opacity 0.6s ease, transform 0.6s ease",
}}
>
{children}
</div>
);
}Lo que demuestra:
threshold: 1.0)triggerOnce para que no se reproduzca nuevamenteuseRef simple, el hook utiliza un callback ref (node) => .... Esto permite que vuelva a observar cuando el elemento de destino cambia (p. ej., renderización condicional)."200px 0px") que expande el área de detección, permitiendo la precarga antes de que el elemento sea visible.0 significa cualquier píxel; 1 significa completamente visible.| Opción | Tipo | Predeterminado | Descripción |
|---|---|---|---|
root | Element o null | null (ventana gráfica) | Ancestro desplazable para usar como ventana gráfica |
rootMargin | string | "0px" | Margen alrededor de raíz para expandir detección |
threshold | number o number[] | 0 | Proporción de visibilidad para disparar |
triggerOnce | boolean | false | Desconectar después de la primera intersección |
enabled | boolean | true | Si se debe observar |
| Retorno | Tipo | Descripción |
|---|---|---|
ref | (node: Element o null) => void | Callback ref para adjuntar a destino |
entry | IntersectionObserverEntry o null | Última entrada de observer |
isIntersecting | boolean | Si el elemento es visible |
Múltiples elementos: Observa muchos elementos con un único observer para mejor rendimiento:
function useIntersectionObserverMultiple(
options: IntersectionObserverInit = {}
) {
const [entries, setEntries] = useState<Map<Element, IntersectionObserverEntry>>(new Map());
const observer = useRef<IntersectionObserver | null>(null);
const observe = useCallback((node: Element) => {
if (!observer.current) {
observer.current = new IntersectionObserver((observed) => {
setEntries((prev) => {
const next = new Map(prev);
observed.forEach((e) => next.set(e.target, e));
return next;
});
}, options);
}
observer.current.observe(node);
}, [options]);
return { observe, entries };
}Con proporción de intersección: Rastrear el porcentaje exacto de visibilidad para animaciones vinculadas al scroll:
const { entry } = useIntersectionObserver({
threshold: Array.from({ length: 101 }, (_, i) => i / 100),
});
const ratio = entry?.intersectionRatio ?? 0; // 0.0 a 1.0Element | null, compatible con cualquier elemento HTML o SVG.IntersectionObserverEntry es un tipo de navegador integrado con isIntersecting, intersectionRatio, boundingClientRect, etc.IntersectionObserver no existe en el servidor. Solución: La guardia typeof IntersectionObserver === "undefined" maneja esto.isIntersecting."10px 20px", no un número. Solución: Valida el formato o documéntalo claramente.| Paquete | Nombre del Hook | Notas |
|---|---|---|
react-intersection-observer | useInView | Más popular, completo |
usehooks-ts | useIntersectionObserver | Simple, basado en ref |
ahooks | useInViewport | Parte de una colección grande |
@uidotdev/usehooks | useIntersectionObserver | Implementación mínima |
framer-motion | useInView | Enfocado en animación |
(node) => ... que React llama cuando el elemento se monta o cambia.useRef, permite que el hook vuelva a observar cuando el elemento de destino se renderiza condicionalmente.Cuando triggerOnce es verdadero y el elemento se vuelve visible, el hook establece una bandera congelada, desconecta el observer y detiene la vigilancia. Esto evita callbacks posteriores, mejorando el rendimiento para contenido cargado perezosamente.
rootMargin es una cadena de margen similar a CSS (p. ej., "200px 0px") que expande el área de detección. Configurar rootMargin: "200px" dispara la intersección 200px antes de que el elemento entre en la ventana gráfica, permitiendo la precarga de imágenes o datos.
0.5 significa 50% visible).[0, 0.25, 0.5, 0.75, 1]).0 significa cualquier píxel visible; 1 significa completamente visible.Los thresholds granulares (p. ej., Array.from({ length: 101 }, (_, i) => i / 100)) disparan el callback 100 veces mientras el elemento se desplaza. Solo usa esto para animaciones vinculadas al scroll. Para lazy loading, threshold: 0 o threshold: 1 es suficiente.
El hook protege con typeof IntersectionObserver === "undefined". En el servidor, retorna temprano sin crear un observer. isIntersecting predeterminado es false.
Usa la variación de múltiples elementos mostrada en la sección Variaciones. Crea un IntersectionObserver y llama .observe(node) para cada elemento. Almacena entradas en un Map indexado por el elemento de destino.
Sí. El callback ref acepta Element | null, que cubre tanto elementos HTML como SVG. IntersectionObserver funciona con cualquier tipo de elemento DOM.
IntersectionObserverEntry es un tipo de navegador integrado con propiedades incluyendo isIntersecting, intersectionRatio, boundingClientRect, rootBounds y target. No se necesitan tipos personalizados.
Cuando enabled es false, el callback ref omite crear un observer. Esto te permite pausar condicionalmente la observación sin desmontar el elemento, y volver a habilitarla más tarde configurando enabled de nuevo a true.
Revisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥