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