Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
import { useEffect, useRef } from "react";
/**
* useEventListener
* Agrega un listener de evento a window, document o cualquier ref de elemento.
* Usa el patrón de ref de callback más reciente para evitar volver a suscribirse en
* cambios de callback. Se limpia automáticamente en desmontar.
*/
// Sobrecarga: eventos de window
function useEventListener<K extends keyof WindowEventMap>(
eventName: K,
handler: (event: WindowEventMap[K]) => void,
element?: undefined,
options?: boolean | AddEventListenerOptions
): void;
// Sobrecarga: eventos de document
function useEventListener<K extends keyof DocumentEventMap>(
eventName: K,
handler: (event: DocumentEventMap[K]) => void,
element: Document,
options?: boolean | AddEventListenerOptions
): void;
// Sobrecarga: eventos de elementos 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;
// Implementación
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 el elemento objetivo
let targetElement: EventTarget;
if (element === undefined) {
// Por defecto es window
if (typeof window === "undefined") return;
targetElement = window;
} else if (element instanceof Document) {
targetElement = element;
} else {
// Es una 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]);
}Cuándo usarlo: Necesitas adjuntar listeners de eventos a window, document o elementos DOM y deseas limpieza automática, callbacks frescos y tipos de eventos type-safe sin escribir boilerplate de addEventListener/removeEventListener.
"use client";
import { useState, useRef } from "react";
// Rastrear estado en línea/fuera de línea
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 ? "En línea" : "Fuera de línea"}
</div>
);
}
// Rastrear posición del ratón en un 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 en document
function KeyLogger() {
const [lastKey, setLastKey] = useState("");
useEventListener(
"keydown",
(event) => {
setLastKey(event.key);
},
document
);
return <p>Última tecla presionada: {lastKey || "ninguna"}</p>;
}
// Desplazamiento con opción 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 }}>
Desplazamiento: {scrollY}px
</div>
);
}Lo que esto demuestra:
online/offline de window sin especificar un elemento (por defecto es window)mousemove de alcance de elemento vía una ref, con MouseEvent tipadokeydown a nivel de document con KeyboardEvent tipado{ passive: true } para rendimientohandlerRef.current(event), así que siempre ejecuta la versión más fresca del handler. Esto elimina cierre obsoleto sin volver a suscribir el listener.window, WindowEventMap tipan el evento. Para refs de elementos, se utiliza HTMLElementEventMap.window (por defecto cuando element es undefined), document (cuando se pasa directamente), o cualquier elemento vía React.RefObject.element es undefined y window no está disponible (SSR), el efecto retorna temprano sin adjuntar nada.options acepta los mismos valores que el addEventListener nativo (booleano para captura, u objeto de opciones con capture, passive, once).| Parámetro | Tipo | Por defecto | Descripción |
|---|---|---|---|
eventName | string | - | Nombre de evento DOM (p. ej., "click", "scroll", "keydown") |
handler | (event: E) => void | - | Handler de evento (tipado por objetivo) |
element | undefined, Document, o RefObject | window | Objetivo del evento |
options | boolean o AddEventListenerOptions | - | Opciones nativas de listener |
Este hook retorna void. Es puramente un hook de efecto secundario.
Con bandera de limpieza: Retorna una función para remover manualmente el listener antes de desmontar:
function useEventListener(eventName, handler, element) {
// ...misma configuración...
const removeRef = useRef<(() => void) | null>(null);
useEffect(() => {
// ...misma lógica...
removeRef.current = () => {
targetElement.removeEventListener(eventName, listener, options);
};
return removeRef.current;
}, [eventName, element, options]);
return { remove: () => removeRef.current?.() };
}Listener de media query: Usa con matchMedia para eventos responsivos:
// Esto es esencialmente lo que useMediaQuery hace internamente
const mql = window.matchMedia("(max-width: 768px)");
useEventListener("change", (e) => setIsMobile(e.matches), { current: mql } as any);Soporte de eventos personalizados: El eventName basado en string funciona con eventos personalizados también:
useEventListener("my-custom-event", (event) => {
console.log((event as CustomEvent).detail);
});window, document y HTMLElement.window para "keydown", el handler se tipea automáticamente como (event: KeyboardEvent) => void.T extends HTMLElement puede ser reducido: useRef<HTMLInputElement>(null).Event (el tipo base) para satisfacer todas las sobrecargas.{ passive: true } para evitar bloquear el thread principal. Solución: Pasa la opción explícitamente cuando escuches scroll, touchstart, o touchmove.{ passive: true } inline), el efecto se vuelve a suscribir cada vez. Solución: Memoiza el objeto de opciones o defínelo fuera del componente.{ capture: true } o true como parámetro de opciones para escuchar en fase de captura.addEventListener crudo sin limpieza, los listeners se acumulan. Solución: Siempre usa este hook o empareja manualmente addEventListener con removeEventListener en useEffect.event.target para identificar el elemento fuente (patrón de delegación de eventos).| Paquete | Nombre del Hook | Notas |
|---|---|---|
usehooks-ts | useEventListener | API sobrecargada similar |
@uidotdev/usehooks | useEventListener | Mínimo, solo window |
ahooks | useEventListener | Soporta cualquier objetivo de evento |
react-use | useEvent | API ligeramente diferente |
@react-aria/interactions | usePress, useHover | Hooks de interacción de alto nivel |
handlerRef.current) asegura que el listener siempre llame a la versión más reciente del handler sin volver a suscribirse.window como objetivo del evento.typeof window === "undefined" y retorna temprano sin adjuntar nada.Pasa un React.RefObject como el tercer argumento:
const ref = useRef<HTMLDivElement>(null);
useEventListener("click", handleClick, ref);
return <div ref={ref}>Click me</div>;Pasa document directamente como el tercer argumento:
useEventListener("keydown", (e) => {
console.log(e.key);
}, document);options en el efecto.useMemo o defínelo como una constante fuera del componente.if (!element.current) return y omite adjuntar el listener.scroll, touchstart, y touchmove para evitar bloquear el thread principal.preventDefault(), permitiendo desplazamiento más suave.eventName basado en string funciona con cualquier nombre de evento, incluyendo eventos personalizados.CustomEvent para acceder a la propiedad detail.event.target para identificar qué elemento hijo gatilló el evento.WindowEventMap, DocumentEventMap, y HTMLElementEventMap.window para "keydown", el handler se tipea automáticamente como (event: KeyboardEvent) => void.Especifica el tipo de elemento al crear la ref:
const inputRef = useRef<HTMLInputElement>(null);
useEventListener("focus", (e) => {
// e es tipado como FocusEvent
}, inputRef);useEffect llama a removeEventListener, que se ejecuta cuando el componente se desmonta o cuando las dependencias cambian.Revisado por Chris St. John·Última actualización: 10 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥