A simple way to manage global DOM events in React with hooks

A simple way to manage global DOM events in React with hooks

Creating UI components like dialog, modal, or drawers mostly requires adding keyboard accessibilities like closing them when ESC (escape) key is pressed, and doing that may require you to attach an event listener for keyup event inside use useEffect hook and as well removing the event listener when the component is destroyed.

So may end up having something like this below where ever you need a global event

useEffect(() => {
  const onESC = (ev: KeyboardEvent) => {
    if (ev.key === "Escape") {
      closeModal();
    }
  };
  window.addEventListener("keyup", onESC, false);
  return () => {
    window.addEventListener("keyup", onESC, false);
  };
}, []);

And I really don't like repeating alike code whenever possible, so let see we can hide most of this code since the only part that might change in different components will be the event handler

const onESC = (ev: KeyboardEvent) => {
    if (ev.key === "Escape") {
      closeModal();
     }
}

So let's start by extracting this to its own component

// ~/hooks/useGlobalDOMEvents.ts
export default function useGlobalDOMEvents() {
  useEffect(() => {
    const onESC = (ev: KeyboardEvent) => {
      if (ev.key === "Escape") {
        closeModal();
      }
    };
    window.addEventListener("keyup", onESC, false);
    return () => {
      window.addEventListener("keyup", onESC, false);
    };
  }, []);
}

Now our main goal is to make this function to accept multiple events and it's handlers, so let's define the type for our props

type Props = {
  [key in keyof WindowEventMap]?: EventListenerOrEventListenerObject;
};

export default function useGlobalDOMEvents(props:Props) {
  useEffect(() => {
    const onESC = (ev: KeyboardEvent) => {
      if (ev.key === "Escape") {
        closeModal();
      }
    };
    window.addEventListener("keyup", onESC, false);
    return () => {
      window.addEventListener("keyup", onESC, false);
    };
  }, []);
}

The props type with dynamic property keys will be very helpful for our editor autocomplete

image.png

Now let's refactor the useEffect block to attach events dynamically based on our props properties


export default function useGlobalDOMEvents(props: Props) {
  useEffect(() => {
    for (let [key, func] of Object.entries(props)) {
      window.addEventListener(key, func, false);
    }
    };
  }, []);
}

and we have to make sure will remove the event listener once the component is destroyed


export default function useGlobalDOMEvents(props: Props = {}) {
  useEffect(() => {
    for (let [key, func] of Object.entries(props)) {
      window.addEventListener(key, func, false);
    }
    return () => {
      for (let [key, func] of Object.entries(props)) {
        window.removeEventListener(key, func, false);
      }
    };
  }, []);
}

and full code will look like this

// ~/hooks/useGlobalDOMEvents.ts
import { useEffect } from "react";

type Props = {
  [key in keyof WindowEventMap]?: EventListenerOrEventListenerObject;
};

export default function useGlobalDOMEvents(props: Props) {
  useEffect(() => {
    for (let [key, func] of Object.entries(props)) {
      window.addEventListener(key, func, false);
    }
    return () => {
      for (let [key, func] of Object.entries(props)) {
        window.removeEventListener(key, func, false);
      }
    };
  }, []);
}

and usage will look like this

export default function Drawer(props: DrawerProps) {
  const { children, open, title, onClose } = props;
 useGlobalDOMEvents({
    keyup(ev: KeyboardEvent) {
      if (ev.key === "Escape") {
        onClose();
      }
    },
  });

[...]
}

I hope you find this helpful.