Screenshot of code for React hooks

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

New Call-to-action
blog comments powered by Disqus
Times
Check

Success!

Times

You're already subscribed

Times