Keyboard Shortcuts with React Hooks

Written by Arthur Tyukayev

In this blog post we will use React's Hooks API to create a custom hook to add keyboard shortcuts to our application. Keyboard shortcuts are a great efficiency feature that allow users to use your application quicker and more efficiently. In React, implementing keyboard shortcuts is quite simple, however with the Hooks API it's trivial and can be done with less than 100 lines of code.


Useful Hooks


In order to properly implement this functionality, we'll be using a combination of three hooks:

  • useEffect allows us to add side effects to our application. In our specific case, we will use this hook to create an event listeners when our component mounts. It will also remove listeners when the component unmounts.
  • useReducer allows us to keep a state of pressed keys. We will use a keydown event to keep track of held down keys and a keyup event to keep track of released keys.
  • useCallback is simply a performance optimization hook which we use to wrap our event listener callback function. If we do not do this, React will re-instantiate a new function every time our custom hook re-renders, which will happen quite often.


Understand the KeyboardEvent


To listen for keypresses in our application, we will be registering and event listen for the keydown/keyup events. This will call our listener function with a KeyboardEvent which has a number of properties that will allow us to build our custom hook. While there are many properties, we will only be focused on two specific properties.

  • KeyboardEvent.repeat is a read only property that returns a Boolean that is set to true if the key is being held down such that it is automatically repeating.
  • KeyboardEvent.key is a read only property that returns a String that is set to the current key that was pressed. This also takes into account any modifier keys that might have affected the KeyboardEvent. For example, holding down the Shift then pressing the number 4, will set the KeyboardEvent.key property to "$" on the US Keyboard.



Creating the hooks


To start we will create a new function and called it as follows:


    
const useKeyboardShortcut = (shortcutKeys, callback) => {}
    
  

Our custom hook will accept two parameters, the first is an array of strings which should match the the KeyboardEvent's key property. The second is a callback function that should be called when the keyboard shortcut is invoked.


Just to ensure that there is no user error, we will add some warnings to our custom hook to make sure that all of the parameter conditions are met:


    
if (!Array.isArray(shortcutKeys))
    throw new Error(
      "The first parameter to `useKeyboardShortcut` must be an ordered array of `KeyboardEvent.key` strings."
    );
 
if (!shortcutKeys.length)
  throw new Error(
    "The first parameter to `useKeyboardShortcut` must contain atleast one `KeyboardEvent.key` string."
  );

if (!callback || typeof callback !== "function")
  throw new Error(
    "The second parameter to `useKeyboardShortcut` must be a function that will be envoked when the keys are pressed."
  );
    
  

Next we will create a state object and a reducer in order to track key presses and releases. Using our shortcutKey's array, we will construct a state object that tracks whether a key is being held down.


    
const initalKeyMapping = shortcutKeys.reduce((currentKeys, key) => {
  currentKeys[key] = false;
  return currentKeys;
}, {});

const [keys, setKeys] = useReducer(keysReducer, initalKeyMapping);
    
  

This will convert an array of keys to an object that is shaped as such:


    
{
  shift: false,
  e: false
} 
    
  

Next we will add two event listeners, one for keydown events and the other for keyup events. Using the useEffect hook, we will add these event listeners on the hooks mount.


    
useEffect(() => {
  window.addEventListener("keydown", keydownListener, true);
  return () => window.removeEventListener("keydown", keydownListener, true);
}, [keydownListener]);

useEffect(() => {
  window.addEventListener("keyup", keyupListener, true);
  return () => window.removeEventListener("keyup", keyupListener, true);
}, [keyupListener]);
    
  

This will add an event listener to the window and remove it when the component using the hook unmounts. As you can see, each event listener is passed a specific handler function.


Keydown Handling


For the keydown event, the handler functions is quite simple:


    
const keydownListener = useCallback(
  keydownEvent => {
    const { key, target, repeat } = keydownEvent;
    if (repeat) return;
    if (blacklistedTargets.includes(target.tagName)) return;
    if (!shortcutKeys.includes(key)) return;

    if (!keys[key])
      setKeys({ type: "set-key-down", key });
  },
  [shortcutKeys, keys]
);
    
  

In order to prevent this function being recreated on every update of this hook, we will wrap the function in useCallback which will only recreate the function if the shortcutKeys or the keys objects have changed.


We check to make sure that this KeyboardEvent is not a repeating event, which means that this is the first key down event of this key. If it's repeating, that means that the key is being held down and has already been processed.


Next we get the key that was pressed and the DOM element that generated this keydown event. We check to make sure that DOM element isn't an input or textarea because we don't want to trigger keyboard shortcuts when the user is typing.


Next we check to make sure that the pressed key is in the shortcutKeys array, this way we are only processing keys are that needed for this keyboard shortcut.


Finally after all of those checks have passed, we fire a set-key-down action with the key which will update the state to indicate the key is being held down.


Keyup Handling


Much like the keyup listener, the keyup makes all of the same checks the only difference is that it will fire the set-key-up action, which will update the state to indicate the key has been released:


    
const keyupListener = useCallback(
  keyupEvent => {
    const { key, target } = keyupEvent;
    if (blacklistedTargets.includes(target.tagName)) return;
    if (!shortcutKeys.includes(key)) return;

    if (keys[key])
      setKeys({ type: "set-key-up", key });
  },
  [shortcutKeys, keys]
);
    
  

Triggering the keyboard callback function


Finally we will create one more useEffect hook that will fire our callback function for the keyboard shortcut. It will check out keys state object to make sure that all of the keys are currently being held down. Once that criteria is met it will fire the callback function.


    
useEffect(() => {
  if (!Object.values(keys).filter(value => !value).length) callback(keys)
}, [callback, keys])
    
  

After everything is complete, the entire keyboard shortcut hook should be about 80 lines of code.


In order to use our keyboard shortcut hook, we simply import the useKeyboardShortcut function and call it in our component.


    
import React, { useState, useCallback } from 'react';
 
import useKeyboardShortcut from 'use-keyboard-shortcut';
 
function App() {
  const [showImage, setShowImage] = useState(false)
  const keys = ['Shift', 'E']
 
  const handleKeyboardShortcut = useCallback(keys => {
    setShowImage(currentShowImage => !currentShowImage)
  }, [setShowImage])
 
  useKeyboardShortcut(keys, handleKeyboardShortcut)
 
  return (
    <div style={styles.main}>
      {showImage && (<img style={styles.image} alt="FullStackLabs Logo" src="/icon.png" />)}
      <h1 style={styles.text}>{`Press ${keys.join(' + ')} to show image.`}</h1>
    </div>
  );
}
 
const styles = {...}
 
export default App;
    
  

In this example, we create a keyboard shortcut that listens for the Shift and E keys, when both of those are pressed, we should see an image appear on the screen.


All of the code in this post can be found here, this package can also be installed from npm.


---
At FullStack Labs, we pride ourselves on our ability to push the capabilities of cutting-edge frameworks like React. Interested in learning more about speeding up development time on your next project? Contact us.

Let’s Talk!

We’d love to learn more about your project. Contact us below for a free consultation with our CEO.
Projects Start at $25,000.

FullStack Labs
This field is required
This field is required
Type of project
Reason for contact:
How did you hear about us? This field is required