Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
import { useState, useEffect, useRef, useCallback } from "react";
interface UseIntersectionObserverOptions {
/** Elemento que é usado como a viewport. Padrão: viewport do navegador */
root?: Element | null;
/** Margem ao redor da root. Padrão: "0px" */
rootMargin?: string;
/** Porcentagem do elemento visível para disparar. 0-1 ou array. Padrão: 0 */
threshold?: number | number[];
/** Dispara apenas uma vez (ex: para lazy loading). Padrão: false */
triggerOnce?: boolean;
/** Começa a observar imediatamente. Padrão: true */
enabled?: boolean;
}
interface UseIntersectionObserverReturn {
/** Ref para anexar ao elemento alvo */
ref: (node: Element | null) => void;
/** A última IntersectionObserverEntry */
entry: IntersectionObserverEntry | null;
/** Se o elemento está atualmente intersectando */
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;
}
}, []);
// Padrão de callback ref para direcionamento flexível de elementos
const ref = useCallback(
(node: Element | null) => {
// Limpa o 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]
);
// Limpeza ao desmontar
useEffect(() => {
return cleanup;
}, [cleanup]);
return {
ref,
entry,
isIntersecting: entry?.isIntersecting ?? false,
};
}Quando usar isso: Você precisa de imagens com lazy loading, gatilhos de scroll infinito, efeitos de animação ao rolar ou rastrear quais seções o usuário rolou.
"use client";
// Imagem com lazy loading
function LazyImage({ src, alt }: { src: string; alt: string }) {
const { ref, isIntersecting } = useIntersectionObserver({
triggerOnce: true,
rootMargin: "200px", // Começa a carregar 200px antes de ficar visível
});
return (
<div ref={ref} style={{ minHeight: 200, background: "#f0f0f0" }}>
{isIntersecting ? (
<img src={src} alt={alt} style={{ width: "100%" }} />
) : (
<div style={{ padding: 20, color: "#999" }}>Carregando...</div>
)}
</div>
);
}
// Gatilho 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);
// Simula chamada de 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" }}>
Item {item}
</div>
))}
<div ref={ref} style={{ padding: 20, textAlign: "center" }}>
{loading ? "Carregando mais..." : "Role para mais"}
</div>
</div>
);
}
// Animação ao rolar
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>
);
}O que isso demonstra:
threshold: 1.0)triggerOnce para que não repitauseRef simples, o hook usa um callback ref (node) => .... Isso permite que ele re-observe quando o elemento alvo muda (ex: renderização condicional)."200px 0px") que expande a área de detecção, permitindo pré-carregamento antes que o elemento fique visível.0 significa qualquer pixel; 1 significa totalmente visível.| Opção | Tipo | Padrão | Descrição |
|---|---|---|---|
root | Element ou null | null (viewport) | Ancestral rolável a ser usado como viewport |
rootMargin | string | "0px" | Margem ao redor da root para expandir a detecção |
threshold | number ou number[] | 0 | Razão de visibilidade para disparar |
triggerOnce | boolean | false | Desconecta após a primeira interseção |
enabled | boolean | true | Se deve observar |
| Retorno | Tipo | Descrição |
|---|---|---|
ref | (node: Element or null) => void | Callback ref para anexar ao alvo |
entry | IntersectionObserverEntry or null | Última entrada do observador |
isIntersecting | boolean | Se o elemento está visível |
Múltiplos elementos: Observe muitos elementos com um único observador para melhor performance:
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 };
}Com razão de interseção: Rastreie a porcentagem exata de visibilidade para animações vinculadas ao scroll:
const { entry } = useIntersectionObserver({
threshold: Array.from({ length: 101 }, (_, i) => i / 100),
});
const ratio = entry?.intersectionRatio ?? 0; // 0.0 a 1.0Element | null, compatível com qualquer elemento HTML ou SVG.IntersectionObserverEntry é um tipo de navegador embutido com isIntersecting, intersectionRatio, boundingClientRect, etc.IntersectionObserver não existe no servidor. Correção: A guarda typeof IntersectionObserver === "undefined" cuida disso.isIntersecting."10px 20px", não um número. Correção: Valide o formato ou documente-o claramente.| Pacote | Nome do Hook | Notas |
|---|---|---|
react-intersection-observer | useInView | Mais popular, com todos os recursos |
usehooks-ts | useIntersectionObserver | Simples, baseado em ref |
ahooks | useInViewport | Parte de uma grande coleção |
@uidotdev/usehooks | useIntersectionObserver | Implementação mínima |
framer-motion | useInView | Focado em animação |
(node) => ... que o React chama quando o elemento monta ou muda.useRef, ele permite que o hook re-observe quando o elemento alvo é renderizado condicionalmente.Quando triggerOnce é verdadeiro e o elemento se torna visível, o hook define um flag congelado, desconecta o observador e para de observar. Isso previne callbacks adicionais, melhorando a performance para conteúdo com lazy loading.
rootMargin é uma string de margem estilo CSS (ex: "200px 0px") que expande a área de detecção. Definir rootMargin: "200px" dispara a interseção 200px antes do elemento entrar na viewport, permitindo o pré-carregamento de imagens ou dados.
0.5 significa 50% visível).[0, 0.25, 0.5, 0.75, 1]).0 significa qualquer pixel visível; 1 significa totalmente visível.Thresholds finamente granulados (ex: Array.from({ length: 101 }, (_, i) => i / 100)) disparam o callback 100 vezes enquanto o elemento rola. Use isso apenas para animações vinculadas ao scroll. Para lazy loading, threshold: 0 ou threshold: 1 é suficiente.
O hook protege com typeof IntersectionObserver === "undefined". No servidor, ele retorna cedo sem criar um observador. isIntersecting tem o valor padrão false.
Use a variação para múltiplos elementos mostrada na seção Variações. Crie um IntersectionObserver e chame .observe(node) para cada elemento. Armazene as entradas em um Map com a chave sendo o elemento alvo.
Sim. O callback ref aceita Element | null, que cobre elementos HTML e SVG. IntersectionObserver funciona com qualquer tipo de elemento DOM.
IntersectionObserverEntry é um tipo de navegador embutido com propriedades incluindo isIntersecting, intersectionRatio, boundingClientRect, rootBounds e target. Nenhum tipo personalizado é necessário.
Quando enabled é false, o callback ref pula a criação de um observador. Isso permite pausar a observação condicionalmente sem desmontar o elemento, e reativá-la depois definindo enabled de volta para true.
Revisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥