Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
import { useEffect, useRef } from "react";
/**
* useEventListener
* Adiciona um listener de evento à janela, documento ou qualquer ref de elemento.
* Usa o padrão de callback ref mais recente para evitar reassinaturas em
* mudanças de callback. Limpa automaticamente ao desmontar.
*/
// Sobrecarga: eventos de janela
function useEventListener<K extends keyof WindowEventMap>(
eventName: K,
handler: (event: WindowEventMap[K]) => void,
element?: undefined,
options?: boolean | AddEventListenerOptions
): void;
// Sobrecarga: eventos de documento
function useEventListener<K extends keyof DocumentEventMap>(
eventName: K,
handler: (event: DocumentEventMap[K]) => void,
element: Document,
options?: boolean | AddEventListenerOptions
): void;
// Sobrecarga: eventos de elemento HTML
function useEventListener<
K extends keyof HTMLElementEventMap,
T extends HTMLElement = HTMLDivElement
>(
eventName: K,
handler: (event: HTMLElementEventMap[K]) => void,
element: React.RefObject<T | null>,
options?: boolean | AddEventListenerOptions
): void;
// Implementação
function useEventListener(
eventName: string,
handler: (event: Event) => void,
element?: Document | React.RefObject<HTMLElement | null>,
options?: boolean | AddEventListenerOptions
): void {
const handlerRef = useRef(handler);
useEffect(() => {
handlerRef.current = handler;
}, [handler]);
useEffect(() => {
// Determina o elemento de destino
let targetElement: EventTarget;
if (element === undefined) {
// Padrão para window
if (typeof window === "undefined") return;
targetElement = window;
} else if (element instanceof Document) {
targetElement = element;
} else {
// É uma ref
if (!element.current) return;
targetElement = element.current;
}
const listener = (event: Event) => handlerRef.current(event);
targetElement.addEventListener(eventName, listener, options);
return () => {
targetElement.removeEventListener(eventName, listener, options);
};
}, [eventName, element, options]);
}Quando usar isso: Você precisa anexar listeners de eventos a window, document ou elementos DOM e deseja limpeza automática, callbacks atualizados e tipos de eventos seguros sem escrever código repetitivo de addEventListener/removeEventListener.
"use client";
import { useState, useRef } from "react";
// Rastreia o status online/offline
function OnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEventListener("online", () => setIsOnline(true));
useEventListener("offline", () => setIsOnline(false));
return (
<div
style={{
padding: 8,
background: isOnline ? "#dcfce7" : "#fee2e2",
borderRadius: 4,
}}
>
{isOnline ? "Online" : "Offline"}
</div>
);
}
// Rastreia a posição do mouse em um elemento
function MouseTracker() {
const ref = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({ x: 0, y: 0 });
useEventListener(
"mousemove",
(event) => {
const rect = ref.current?.getBoundingClientRect();
if (rect) {
setPosition({
x: event.clientX - rect.left,
y: event.clientY - rect.top,
});
}
},
ref
);
return (
<div
ref={ref}
style={{
width: 300,
height: 200,
background: "#f5f5f5",
border: "1px solid #ccc",
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "crosshair",
}}
>
x: {position.x}, y: {position.y}
</div>
);
}
// Eventos de teclado no documento
function KeyLogger() {
const [lastKey, setLastKey] = useState("");
useEventListener(
"keydown",
(event) => {
setLastKey(event.key);
},
document
);
return <p>Última tecla pressionada: {lastKey || "nenhuma"}</p>;
}
// Scroll com opção passive
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
useEventListener(
"scroll",
() => setScrollY(window.scrollY),
undefined,
{ passive: true }
);
return (
<div style={{ position: "fixed", top: 0, right: 0, padding: 8 }}>
Scroll: {scrollY}px
</div>
);
}O que isso demonstra:
online/offline da janela sem especificar um elemento (padrão para janela)mousemove com escopo de elemento via ref, com MouseEvent tipadokeydown em nível de documento com KeyboardEvent tipado{ passive: true } para desempenhohandlerRef.current(event), então ele sempre executa a versão mais recente do manipulador. Isso elimina closures obsoletos sem re-assinar o listener.window, WindowEventMap tipa o evento. Para refs de elementos, HTMLElementEventMap é usado.window (padrão quando element é indefinido), document (quando passado diretamente) ou qualquer elemento via React.RefObject.element é indefinido e window não está disponível (SSR), o efeito retorna cedo sem anexar nada.options aceita os mesmos valores do addEventListener nativo (booleano para capture, ou um objeto de opções com capture, passive, once).| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
eventName | string | - | Nome do evento DOM (ex., "click", "scroll", "keydown") |
handler | (event: E) => void | - | Manipulador de eventos (tipado por destino) |
element | undefined, Document, ou RefObject | window | Destino do evento |
options | boolean or AddEventListenerOptions | - | Opções nativas do listener |
Este hook retorna void. É puramente um hook de efeito colateral.
Com flag de limpeza: Retorna uma função para remover manualmente o listener antes de desmontar:
function useEventListener(eventName, handler, element) {
// ...mesma configuração...
const removeRef = useRef<(() => void) | null>(null);
useEffect(() => {
// ...mesma lógica...
removeRef.current = () => {
targetElement.removeEventListener(eventName, listener, options);
};
return removeRef.current;
}, [eventName, element, options]);
return { remove: () => removeRef.current?.() };
}Listener de media query: Use com matchMedia para eventos responsivos:
// Isso é essencialmente o que useMediaQuery faz internamente
const mql = window.matchMedia("(max-width: 768px)");
useEventListener("change", (e) => setIsMobile(e.matches), { current: mql } as any);Suporte a eventos personalizados: O eventName baseado em string funciona com eventos personalizados também:
useEventListener("my-custom-event", (event) => {
console.log((event as CustomEvent).detail);
});window, document e HTMLElement.window, o manipulador recebe o subtipo de evento correto (ex., KeyboardEvent para "keydown", MouseEvent para "click").T extends HTMLElement pode ser restringido: useRef<HTMLInputElement>(null).Event (o tipo base) para satisfazer todas as sobrecargas.{ passive: true } para evitar bloquear a thread principal. Correção: Passe a opção explicitamente ao ouvir scroll, touchstart ou touchmove.{ passive: true } inline), o efeito re-assina a cada vez. Correção: Use memoize do objeto de opções com useMemo ou defina-o como uma constante fora do componente.{ capture: true } ou true como parâmetro de opções para escuta na fase de captura.addEventListener bruto sem limpeza, os listeners se acumulam. Correção: Sempre use este hook ou combine manualmente addEventListener com removeEventListener em useEffect.event.target para identificar o elemento de origem (padrão de delegação de eventos).| Pacote | Nome do Hook | Notas |
|---|---|---|
usehooks-ts | useEventListener | API sobrecarregada semelhante |
@uidotdev/usehooks | useEventListener | Mínimo, apenas para janela |
ahooks | useEventListener | Suporta qualquer destino de evento |
react-use | useEvent | API ligeiramente diferente |
@react-aria/interactions | usePress, useHover | Hooks de interação de alto nível |
handlerRef.current) garante que o listener sempre chame a versão mais recente do manipulador sem re-assinar.window como destino do evento por padrão.typeof window === "undefined" e retorna cedo sem anexar nada.Passe um React.RefObject como terceiro argumento:
const ref = useRef<HTMLDivElement>(null);
useEventListener("click", handleClick, ref);
return <div ref={ref}>Clique em mim</div>;Passe document diretamente como terceiro argumento:
useEventListener("keydown", (e) => {
console.log(e.key);
}, document);options no efeito.useMemo ou defina-o como uma constante fora do componente.if (!element.current) return e pula a anexação do listener.scroll, touchstart e touchmove para evitar bloquear a thread principal.preventDefault(), permitindo um scroll mais suave.eventName baseado em string funciona com qualquer nome de evento, incluindo eventos personalizados.CustomEvent para acessar a propriedade detail.event.target para identificar qual elemento filho acionou o evento.WindowEventMap, DocumentEventMap e HTMLElementEventMap.window para "keydown", o manipulador é tipado como (event: KeyboardEvent) => void automaticamente.Especifique o tipo do elemento ao criar a ref:
const inputRef = useRef<HTMLInputElement>(null);
useEventListener("focus", (e) => {
// e é tipado como FocusEvent
}, inputRef);useEffect chama removeEventListener, que é executada quando o componente é desmontado ou quando as dependências mudam.Revisado por Chris St. John·Última atualização: 10 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥