DOM

useEventListener

Subscribe to a window, document or element event with cleanup.

Demo

Click here, or press any key
last key (window)
key presses
0
panel clicks
0

Usage

use-event-listener.tsxtsx
import { useRef, useState } from "react";
import { useEventListener } from "hookli";

export function Demo() {
  const ref = useRef<HTMLDivElement>(null);
  const [lastKey, setLastKey] = useState("");

  useEventListener("keydown", (event) => setLastKey(event.key));
  useEventListener("click", () => console.log("clicked"), ref);

  return <div ref={ref}>Last key: {lastKey}</div>;
}

API

useEventListener.d.tsts
useEventListener<K>(eventName: K, handler: (event: Event) => void, element?: RefObject<T>, options?: boolean | AddEventListenerOptions): void

Parameters

NameTypeDefaultDescription
eventNameKThe event to listen for, typed against the target's event map (window, document, media query, or element).
handler(event) => voidCalled with the typed event on every dispatch. Held in a ref, so updating it never detaches and re-attaches the listener.
elementRefObject<T>windowOptional ref to the target. Defaults to window when omitted.
optionsboolean | AddEventListenerOptionsStandard addEventListener options (capture, passive, once).

Returns

This hook returns nothing.

Hook

The implementation, straight from hookli. Copy it, or install the package.

src/hooks/use-event-listener/use-event-listener.tstsx
import { RefObject, useEffect, useRef } from "react";
import { useIsomorphicLayoutEffect } from "../use-isomorphic-layout-effect/use-isomorphic-layout-effect";

function useEventListener<K extends keyof MediaQueryListEventMap>(
  eventName: K,
  handler: (event: MediaQueryListEventMap[K]) => void,
  element: RefObject<MediaQueryList>,
  options?: boolean | AddEventListenerOptions,
): void;
function useEventListener<K extends keyof WindowEventMap>(
  eventName: K,
  handler: (event: WindowEventMap[K]) => void,
  element?: undefined,
  options?: boolean | AddEventListenerOptions,
): void;
function useEventListener<K extends keyof DocumentEventMap>(
  eventName: K,
  handler: (event: DocumentEventMap[K]) => void,
  element: RefObject<Document>,
  options?: boolean | AddEventListenerOptions,
): void;
function useEventListener<
  K extends keyof HTMLElementEventMap & keyof SVGElementEventMap,
  T extends HTMLElement | SVGElement = HTMLDivElement,
>(
  eventName: K,
  handler: (event: HTMLElementEventMap[K] | SVGElementEventMap[K]) => void,
  element: RefObject<T>,
  options?: boolean | AddEventListenerOptions,
): void;
function useEventListener(
  eventName: string,
  handler: (event: Event) => void,
  element?: RefObject<HTMLElement | MediaQueryList | Document>,
  options?: boolean | AddEventListenerOptions,
) {
  // Hold the handler in a ref so updating it never detaches the listener.
  const savedHandler = useRef(handler);

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

  useEffect(() => {
    const targetElement = element?.current ?? window;
    if (!targetElement?.addEventListener) return;

    const listener = (event: Event) => savedHandler.current(event);
    targetElement.addEventListener(eventName, listener, options);

    return () => {
      targetElement.removeEventListener(eventName, listener, options);
    };
  }, [eventName, element, options]);
}

export { useEventListener };

View source on GitHub

Related hooks