Skip to main content

All hooks

useEventListener

useEventListener hook for React apps.


Supports Window, Element, Document, and MediaQueryList, with almost the same parameters as the native addEventListener options.

TypeScript

The hook is declared with several overloads. TypeScript picks one from:

  1. Window — omit the element argument. eventName must be a key of WindowEventMap; the handler receives WindowEventMap[eventName].
  2. Document — pass a ref whose current is Document (e.g. useRef<Document>(document)). Use keys of DocumentEventMap for eventName.
  3. HTMLElement / SVG — pass useRef<HTMLButtonElement>(null) (or another element type). Use keys shared by HTMLElementEventMap and SVGElementEventMap.
  4. Media query — pass a ref to a MediaQueryList and use change (see MediaQueryListEventMap).

If the third argument is omitted, the listener attaches to window. If you pass a ref, attach to ref.current when it is non-null; otherwise the effect falls back to window (see implementation).

Custom events

If you want to use a CustomEvent with TypeScript, extend the right map:

  • MediaQueryListEventMap
  • WindowEventMap
  • HTMLElementEventMap
  • DocumentEventMap

Example:

declare global {
  interface DocumentEventMap {
    'my-custom-event': CustomEvent<{ exampleArg: string }>
  }
}

Usage

import { useRef } from 'react'

import { useEventListener } from './useEventListener'

export default function Component() {
  const buttonRef = useRef<HTMLButtonElement>(null)
  const documentRef = useRef<Document>(document)

  const onScroll = (event: WindowEventMap['scroll']) => {
    console.log('window scrolled!', event.type)
  }

  const onClick = (event: HTMLElementEventMap['click']) => {
    console.log('button clicked!', event.currentTarget)
  }

  const onVisibilityChange = (event: DocumentEventMap['visibilitychange']) => {
    console.log('doc visibility changed!', {
      isVisible: !document.hidden,
      event,
    })
  }

  useEventListener('scroll', onScroll)

  useEventListener('visibilitychange', onVisibilityChange, documentRef)

  useEventListener('click', onClick, buttonRef)

  return (
    <div style={{ minHeight: '200vh' }}>
      <button ref={buttonRef} type="button">
        Click me
      </button>
    </div>
  )
}

API

function useEventListener(eventName: K, handler: (event: unknown) => void, element: RefObject<MediaQueryList | null>, options?: boolean | AddEventListenerOptions): void

Parameters

NameTypeDefault valueDescription
eventNameK--
handler(event: unknown) => void--
element`RefObject<MediaQueryListnull>`-
options?`booleanAddEventListenerOptions`-

Returns

No return description available yet.

Hook

import { useEffect, useRef } from 'react'
import type { RefObject } from 'react'

import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect/useIsomorphicLayoutEffect'

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

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

// Element Event based useEventListener interface
function useEventListener<
  K extends keyof HTMLElementEventMap & keyof SVGElementEventMap,
  T extends Element = K extends keyof HTMLElementEventMap ? HTMLDivElement : SVGElement,
>(
  eventName: K,
  handler: ((event: HTMLElementEventMap[K]) => void) | ((event: SVGElementEventMap[K]) => void),
  element: RefObject<T | null>,
  options?: boolean | AddEventListenerOptions,
): void

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

/**
 * Custom hook that attaches event listeners to DOM elements, the window, or media query lists.
 * @template KW - The type of event for window events.
 * @template KH - The type of event for HTML or SVG element events.
 * @template KM - The type of event for media query list events.
 * @template T - The type of the DOM element (default is `HTMLElement`).
 * @param {KW | KH | KM} eventName - The name of the event to listen for.
 * @param {(event: WindowEventMap[KW] | HTMLElementEventMap[KH] | SVGElementEventMap[KH] | MediaQueryListEventMap[KM] | Event) => void} handler - The event handler function.
 * @param {RefObject<T>} [element] - The DOM element or media query list to attach the event listener to (optional).
 * @param {boolean | AddEventListenerOptions} [options] - An options object that specifies characteristics about the event listener (optional).
 * @public
 * @see [Documentation](https://usehooks-ts.com/react-hook/use-event-listener)
 * @example
 * ```tsx
 * const handleResize = (event: WindowEventMap['resize']) => {
 *   console.log(window.innerWidth);
 * };
 * useEventListener('resize', handleResize);
 * ```
 * @example
 * ```tsx
 * const documentRef = useRef<Document>(document);
 * const handleVisibility = (event: DocumentEventMap['visibilitychange']) => {
 *   console.log(document.hidden, event);
 * };
 * useEventListener('visibilitychange', handleVisibility, documentRef, { capture: true });
 * ```
 * @example
 * ```tsx
 * const buttonRef = useRef<HTMLButtonElement>(null);
 * const handleClick = (event: HTMLElementEventMap['click']) => {
 *   console.log(event.currentTarget);
 * };
 * useEventListener('click', handleClick, buttonRef);
 * ```
 */
function useEventListener<
  KW extends keyof WindowEventMap,
  KH extends keyof HTMLElementEventMap & keyof SVGElementEventMap,
  KM extends keyof MediaQueryListEventMap,
  T extends HTMLElement | SVGAElement | MediaQueryList = HTMLElement,
>(
  eventName: KW | KH | KM,
  handler: (
    event:
      | WindowEventMap[KW]
      | HTMLElementEventMap[KH]
      | SVGElementEventMap[KH]
      | MediaQueryListEventMap[KM]
      | Event,
  ) => void,
  element?: RefObject<T | null>,
  options?: boolean | AddEventListenerOptions,
) {
  // Create a ref that stores handler
  const savedHandler = useRef(handler)

  useIsomorphicLayoutEffect(() => {
    savedHandler.current = handler
  }, [handler])

  useEffect(() => {
    // Define the listening target
    const targetElement: T | Window = element?.current ?? window

    if (!(targetElement && targetElement.addEventListener)) return

    // Create event listener that calls handler function stored in ref
    const listener: typeof handler = event => {
      savedHandler.current(event)
    }

    targetElement.addEventListener(eventName, listener, options)

    // Remove event listener on cleanup
    return () => {
      targetElement.removeEventListener(eventName, listener, options)
    }
  }, [eventName, element, options])
}

export { useEventListener }