useEventListener
Cuándo usarlo
Sección titulada «Cuándo usarlo»- Escuchar eventos de
window(scroll, resize, keydown) desde un componente, con limpieza garantizada. - Suscribirse a un evento de un elemento del DOM vía
refsin escribiruseEffect+addEventListener+removeEventListenercada vez. - Reemplazar los
useEffectrepetidos que solo existen para un listener — este hook es eseuseEffect, escrito una vez y reutilizado.
import { type RefObject, useEffect, useRef } from "react"
type Target = Window | HTMLElement
export function useEventListener<K extends keyof WindowEventMap>( eventName: K, handler: (event: WindowEventMap[K]) => void, target?: RefObject<HTMLElement | null>): voidexport function useEventListener( eventName: string, handler: (event: Event) => void, target?: RefObject<HTMLElement | null>) { // El handler vive en un ref: el efecto no necesita re-suscribirse // solo porque el componente pasó una función nueva en este render. const savedHandler = useRef(handler)
useEffect(() => { savedHandler.current = handler }, [handler])
useEffect(() => { const el: Target | null = target ? target.current : window if (!el || !el.addEventListener) return
function eventListener(event: Event) { savedHandler.current(event) }
el.addEventListener(eventName, eventListener) return () => el.removeEventListener(eventName, eventListener) }, [eventName, target])}// Sobre window (sin target)useEventListener("keydown", (event) => { if (event.key === "Escape") cerrarModal()})
// Sobre un elemento puntual (con ref)const buttonRef = useRef<HTMLButtonElement>(null)useEventListener("mouseenter", () => setHovered(true), buttonRef)El overload tipado (K extends keyof WindowEventMap) hace que event en el callback tenga el tipo correcto según el nombre del evento — 'keydown' te da KeyboardEvent, 'mouseenter' te da MouseEvent, sin castear nada a mano.
Targets, limpieza y closures
Sección titulada «Targets, limpieza y closures»- El
handleren unref(en vez de directo en las dependencias del segundouseEffect) es lo que evita quitar y volver a poner el listener en cada render cuando el componente pasa una función inline — el listener real (eventListener) se registra una sola vez poreventName/target, y siempre llama a la versión más reciente del handler a través del ref. targetes opcional: sin él, escucha enwindow— el caso más común (scroll, resize, teclas globales). Con unref, escucha en ese elemento puntual.- Ojo con un
refque todavía no existe: sitarget.currentesnullcuando corre el efecto (el elemento se monta condicionalmente, o el ref se asigna después), el hook no engancha nada — y como la identidad del objetorefno cambia cuando.currentsí, el efecto no se vuelve a ejecutar solo. Si el elemento puede aparecer después del montaje inicial, necesitas otra señal en las dependencias (unuseStateque marque “ya montado”, por ejemplo) para forzar la re-suscripción. - No lo uses para eventos de React ya cubiertos por props (
onClick,onChange) — sirve para eventos que no tienen equivalente como prop de JSX, o para escuchar fuera del elemento que renderiza el componente.
Comprobación
Sección titulada «Comprobación»Monta y desmonta el componente varias veces: una tecla debe ejecutar un solo callback. Con un ref vacío no debe instalarse un listener global. Cambia un valor que lea el callback y verifica que no quede congelado. Este contrato cubre window y un ref de elemento; para document o nodos que cambian durante el montaje, adapta una API con target reactivo explícito.