Join our Discord Community

useEventListener

React hook that attaches event listeners to DOM elements, the window, or media query lists with automatic cleanup and type safety.

Loading component...

Installation

npx shadcn@latest add https://www.shadcn.io/registry/use-event-listener.json
npx shadcn@latest add https://www.shadcn.io/registry/use-event-listener.json
pnpm dlx shadcn@latest add https://www.shadcn.io/registry/use-event-listener.json
bunx shadcn@latest add https://www.shadcn.io/registry/use-event-listener.json

Features

  • Versatile targets - Attach to window, document, DOM elements, or media query lists
  • Automatic cleanup - Event listeners are removed on unmount or dependency changes
  • Type safety - Full TypeScript support with proper event type inference
  • Performance optimized - Handler function is stored in ref to prevent unnecessary re-renders
  • Multiple overloads - Different interfaces for different event targets
  • Modern API - Uses standard addEventListener/removeEventListener

Usage

import { useRef } from "react"
import { useEventListener } from "@/hooks/use-event-listener"

function MyComponent() {
  const buttonRef = useRef<HTMLButtonElement>(null)

  // Window event
  useEventListener("scroll", (event) => {
    console.log("Window scrolled!", event)
  })

  // Element event
  useEventListener("click", (event) => {
    console.log("Button clicked!", event)
  }, buttonRef)

  return <button ref={buttonRef}>Click me</button>
}

API Reference

useEventListener

The hook has multiple overloads for different event targets:

Window Events

useEventListener<K extends keyof WindowEventMap>(
  eventName: K,
  handler: (event: WindowEventMap[K]) => void,
  element?: undefined,
  options?: boolean | AddEventListenerOptions,
): void

Element Events

useEventListener<K, T extends Element>(
  eventName: K,
  handler: (event: HTMLElementEventMap[K] | SVGElementEventMap[K]) => void,
  element: RefObject<T>,
  options?: boolean | AddEventListenerOptions,
): void

Document Events

useEventListener<K extends keyof DocumentEventMap>(
  eventName: K,
  handler: (event: DocumentEventMap[K]) => void,
  element: RefObject<Document>,
  options?: boolean | AddEventListenerOptions,
): void

MediaQueryList Events

useEventListener<K extends keyof MediaQueryListEventMap>(
  eventName: K,
  handler: (event: MediaQueryListEventMap[K]) => void,
  element: RefObject<MediaQueryList>,
  options?: boolean | AddEventListenerOptions,
): void

Usage Examples

Window Events

const [scrollY, setScrollY] = useState(0)

useEventListener("scroll", () => {
  setScrollY(window.scrollY)
})

useEventListener("resize", () => {
  console.log("Window resized to:", window.innerWidth, window.innerHeight)
})

Element Events

const divRef = useRef<HTMLDivElement>(null)
const [mousePos, setMousePos] = useState({ x: 0, y: 0 })

useEventListener("mousemove", (event) => {
  setMousePos({ x: event.clientX, y: event.clientY })
}, divRef)

useEventListener("click", (event) => {
  console.log("Div clicked at:", event.clientX, event.clientY)
}, divRef)

Document Events

const documentRef = useRef<Document>(document)
const [isVisible, setIsVisible] = useState(!document.hidden)

useEventListener("visibilitychange", () => {
  setIsVisible(!document.hidden)
}, documentRef)

useEventListener("keydown", (event) => {
  if (event.key === "Escape") {
    console.log("Escape key pressed")
  }
}, documentRef)

MediaQueryList Events

const mediaQueryRef = useRef(window.matchMedia("(max-width: 768px)"))
const [isMobile, setIsMobile] = useState(false)

useEventListener("change", (event) => {
  setIsMobile(event.matches)
}, mediaQueryRef)

With Event Options

// Passive event listener for better performance
useEventListener("scroll", handleScroll, undefined, { passive: true })

// Once option - listener runs only once
useEventListener("load", handleLoad, undefined, { once: true })

// Capture phase
useEventListener("click", handleClick, elementRef, { capture: true })

Complex Event Handling

function KeyboardHandler() {
  const [keys, setKeys] = useState<string[]>([])

  useEventListener("keydown", (event) => {
    setKeys(prev => [...prev, event.key].slice(-5)) // Keep last 5 keys
  })

  useEventListener("keyup", (event) => {
    if (event.key === "Escape") {
      setKeys([]) // Clear on escape
    }
  })

  return <div>Last keys: {keys.join(", ")}</div>
}

Conditional Event Listeners

function ConditionalListener({ enabled }: { enabled: boolean }) {
  const buttonRef = useRef<HTMLButtonElement>(null)

  // Only attach listener when enabled
  useEventListener(
    "click",
    () => console.log("Clicked!"),
    enabled ? buttonRef : undefined
  )

  return <button ref={buttonRef}>Click me</button>
}

Common Use Cases

  • Scroll tracking - Monitor window or element scroll positions
  • Keyboard shortcuts - Global or element-specific key handlers
  • Mouse interactions - Track mouse movements, clicks, hover states
  • Visibility tracking - Detect when page/elements become visible
  • Resize handling - Respond to window or element size changes
  • Media queries - React to breakpoint changes programmatically
  • Form interactions - Handle input, focus, blur events
  • Touch events - Mobile touch gesture handling

Implementation Details

  • Handler function is stored in a ref to prevent unnecessary effect re-runs
  • Uses useIsomorphicLayoutEffect to update handler ref synchronously
  • Automatically adds and removes event listeners based on dependencies
  • Falls back to window when no element is provided
  • Supports all standard addEventListener options
  • Provides full TypeScript intellisense for event types and properties
  • Handles cleanup automatically on unmount or dependency changes