Managing global DOM events in React with Hooks
Tags: react, javascript, html, typescript
Published: 2020-12-04 09:21:44

Creating UI components like dialog, modal, or drawers mostly requires adding keyboard accessibilities like closing them when ESC (escape) key is pressed, and doing so may require you to attach an event listener on Window object 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

```ts
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

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

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

```ts
// ~/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

```ts
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

![Screenshot](https://dev-to-uploads.s3.amazonaws.com/i/p0k8x93chnspbypfasti.png)

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

```ts

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

```ts

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

```ts
// ~/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


```ts
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.