Build a useKeypress Hook in React

One of the great patterns to come out of React 16.8 is composable hooks. Using React's built-in hooks such as useState and useEffect, we can encapsulate and modularize bits of functionality — almost the same way we create reusable components. In this article we're going to throw together a quick hook that registers an event listener on a keypress and performs an action. Nothing fancy, but it's a nice abstraction that makes it a lot easier to add some nice power-user features to your app.
The goal of our hook isn't to replace the existing synthetic event [onKeypress]{.title-ref} — that's easy enough to register on an element in your JSX. This hook registers a listener when a component mounts and performs an action when the chosen key is pressed. This is useful for things like modals or menus that you want to be able to close using the "escape" key.
To start, let's create a new file. Add /src/hooks/useKeypress.js
.
Inside useKeypress.js
, we're going to create an empty function and
export it, define arguments, and write our jsdocs.
/**
* useKeyPress
* @param {string} key - the name of the key to respond to, compared against event.key
* @param {function} action - the action to perform on key press
*/
export default function useKeypress(key, action) {
// TODO: implement
}
Our useKeypress
hook expects to receive a key to register and an
action to perform. If you've never used
JSDoc, I recommend it as
a great way to document your javascript.
Now we need to register an event listener. For that we'll need to
utilize the useEffect
hook shipped with React 16.8. useEffect
is a
hook that executes a callback when a component mounts. You can link that
action to props or state for more fine-grained control. We'll be
utilizing all three major features of the useEffect
hook, so more on
that later.
Let's import and call useEffect
:
import { useEffect } from "react";
// JSDoc stuff
export default function useKeypress(key, action) {
useEffect(() => {
// TODO: define event listener
// TODO: register event listener
// TODO: unregister event listener
// TODO: link to component lifecycle
});
}
We've laid out our remaining tasks inside the body of the callback
fired by useEffect
. Let's start with our first item: defining the
event listener. This will be a really basic function that executes the
action
argument if the right key is pressed:
// imports and JSDoc
useEffect(() => {
function onKeyup(e) {
if (e.key === key) action();
}
});
We've named our listener onKeyup
for reasons that will become clear
later. Event
objects sent by Keypress
events contain a few special
entries, and in particular we're interested in the key
entry. In the
past, developers would typically inspect the keyCode
entry, but it is
no longer recommended to do so. Event.key
is meant to be a
cross-platform compatible abstraction of keyboard keys.
The next step is to register our listener:
// imports and JSDoc
useEffect(() => {
function onKeyup(e) {
if (e.key === key) action();
}
window.addEventListener("keyup", onKeyup);
});
In case you're curious, listening for keypress
is not recommended. It
only fires for keys that produce a character value (i.e. alphanumeric
keys), and ignores keys like "Escape" and "Enter." It’s convention
to name your event listeners “on” + the event name. Thus, we named our
function “onKeyup.”
At this point we have functionality, but our code is still broken. Using this hook now would cause another event listener to be registered on every render. That's a memory leak!
Luckily, useEffect
comes with a handy feature just for such a thing:
the cleanup function. The callback inside of useEffect is meant to
perform side-effects, but if we return a function from our callback,
that function will be executed when the component unmounts. This makes
it a great place to perform any "cleanup" necessary.
There's one last thing missing from our useEffect
and our hook. We
need to provide, as a second argument to useEffect
, our dependency
array. This is the part of the useEffect
hook that tells React which
bits of state our side-effects are linked to. If not provided,
useEffect
will call your function on every render. If we were to
provide an array with our key
argument, useEffect
would fire any
time that value changed. In our case, we want useEffect
to fire only
on initial mount — not on any additional renders or state changes. To
achieve that, we can provide an empty array as the second argument to
useEffect
.
Our hook is now complete!
import { useEffect } from "react";
/**
* useKeyPress
* @param {string} key - the name of the key to respond to, compared against event.key
* @param {function} action - the action to perform on key press
*/
export default function useKeypress(key, action) {
useEffect(() => {
function onKeyup(e) {
if (e.key === key) action();
}
window.addEventListener("keyup", onKeyup);
return () => window.removeEventListener("keyup", onKeyup);
}, []);
}
And that's it! Using our hook looks like this:
import useKeypress from "src/hooks/useKeypress";
const MyComponent = (props) => {
useKeypress("Escape", () => {
alert("you pressed escape!");
});
return <h1>Example</h1>;
};
React 16.8 introduced hooks to the React community, and it has changed the way we build our apps. React has always been about creating modular, reusable components for use across codebases. Hooks make it easy to create reusable logic that taps into state, props, and the component lifecycle.