How to Build a Custom React Hook to Listen for Keyboard Events
Tags: javascript, a11y, react, frontend
Published: 2023-11-13 07:14:20

I recently built a neat little slide-out menu bar for an improved mobile viewing experience on my personal website.

![Demonstration of Slide Out Menu Bar](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ruielditryui2ps5gt12.gif)

> This is a compressed gif; it is way smoother in action.

Part of my quest to make my work as accessible as possible includes navigating my site nicely using keyboard events. So, I decided to add a little keyboard shortcut to close my open menu when the `Escape` key is pressed.

Instead of jumping to a library that would implement this trivial change, I opted to build out this functionality myself, which gave me more control over exactly how it would work.

The end result is this simple `useCloseMenuOnEscape` hook that takes in values based on my `isMenuOpen` value and my `toggleMenu` function.

```jsx
export function Navigation() {
  const { isMenuOpen, toggleMenu } = useLayoutStore();

  useCloseMenuOnEscape({ isMenuOpen, toggleMenu });

  return <>The part that is irrelevant to this post</>;
}
```

## Under the Hook - Excuse the Pun

Let's dive into my `useCloseMenuOnEscape` hook:

```tsx
import { useEffect } from "react";

interface UseCloseMenuOnEscapeArgs {
  isMenuOpen: boolean;
  toggleMenu: () => void;
}

export function useCloseMenuOnEscape({
  isMenuOpen,
  toggleMenu,
}: UseCloseMenuOnEscapeArgs) {
  useEffect(() => {
    if (!isMenuOpen) {
      return;
    }

    function keyDownHandler(e: globalThis.KeyboardEvent) {
      if (isMenuOpen && e.key === "Escape") {
        e.preventDefault();
        toggleMenu();
      }
    }

    document.addEventListener("keydown", keyDownHandler);

    return () => {
      document.removeEventListener("keydown", keyDownHandler);
    };
  }, [isMenuOpen]);
}
```

As you can see, it's actually all just a simple `useEffect` hook that does the following:

1. It runs whenever the `isMenuOpen` prop changes.
2. If the `isMenuOpen` prop is `false`, it doesn't do anything.
3. If the `isMenuOpen` prop is `true`, it adds an event listener that will call the `toggleMenu` function if `Escape` is pressed.
4. If the component that this hook is running on is unmounted, or the `isMenuOpen` prop changes, the event listener is removed.

This logic extraction to a custom hook is a great way to contain specific business logic in its own module. As you can see from my code snippet of the `Navigation` component above, it is much easier to read what is going on using this abstraction.

## A More General Use Case Hook

My use case above was rather specific, but if you need to create a more generalized reusable hook, you could create this:

```tsx
import { useEffect } from "react";

interface UseKeyboardShortcutArgs {
  key: string
  onKeyPressed: () => void;
}

export function useKeyboardShortcut({
  key,
  onKeyPressed
}: UseCloseMenuOnEscapeArgs) {
  useEffect(() => {
    function keyDownHandler(e: globalThis.KeyboardEvent) {
      if (e.key === key) {
        e.preventDefault();
        onKeyPressed();
      }
    }

    document.addEventListener("keydown", keyDownHandler);

    return () => {
      document.removeEventListener("keydown", keyDownHandler);
    };
  }, []);
}
```

In this scenario, you have a hook that you can use that accepts a `key` prop used to define what keyboard key you're listening for. It also accepts the `onKeyPressed`, which defines what will run if the specified `key` is pressed.

You can run this hook in your component like this:

```jsx
function MyComponent() {
  useKeyboardShortcut({
    key: "Enter",
    onKeyPressed: () => console.log("Enter was pressed!"),
  })

  return <>whatever irrelevant thing you want to render</>
}
```

In this simple example, whenever `MyComponent` is mounted, an event listener is added that listens for the key `Enter`. Once `Enter` is pressed, it will run `onKeyPressed`, which in this case is a console log that prints out `"Enter was pressed!"`.

## Conclusion

I hope this post has given you insight into how you can create your own keyboard event listening hooks rather than jumping to a library.

Building your own solutions in this context gives you a finer grain of control over what you'd like to happen without adding another dependency to your project.