React Keyboard Listener Banner
Barry Michael Doyle Profile Picture
Barry Michael Doyle
Posted on Nov 13, 2023 • Updated on Nov 13, 2023

How to Build a Custom React Hook to Listen for Keyboard Events

#a11y#JavaScript#ReactJS#TypeScript

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

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.

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:

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:

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:

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.