DEV Community

loading...
Cover image for Homebrew React Hooks: useIsKeyPressed

Homebrew React Hooks: useIsKeyPressed

Laurin Quast
・Updated on ・4 min read

Cover Art by Enrique Grisales

It has been a while in this series...

But now I am picking it up again ☺️.

The first two entries in this series were a bit complex, so in order to get things rolling again, I will start with a simple, but also a bit tricky hook (Spoiler: Cross-Platform issues πŸ˜–)!

Ever wanted to know whether a key is pressed? useIsKeyPressed will be your friend πŸ‘Œ.

Let's start with defining the interface of our hook:

The input should be a string that identifies the key, such as a, Alt, Enter.

The return value should be a boolean that indicates whether the key is currently pressed or not.

Key is pressed: return true
Key is not pressed: return false

Okay, let's check the APIs we are going to use!

We will use the keydown and keyup events for our event handlers.

Usually, we attach event listeners in React by passing a Function to the HTML primitive JSX element.

import React from "react";

const MyComponent = () => {
  const handler = () => {
    console.log("hey")
  }
  return <div onKeyDown={handler} />
}
Enter fullscreen mode Exit fullscreen mode

However, this will only trigger the event listener in case the events are triggered within the element. For our use-case, we gonna implement a global event listener that is registered on the global Window object.

Let's build our hook:

import React from "react";

const useIsKeyPressed = (key) => {
  const [isKeyPressed, setIsKeyPressed] = React.useState(false);

  React.useEffect(() => {
    setIsKeyPressed(false);
    const onKeyDown = (ev) => {
      if (ev.key === key) setIsKeyPressed(true);
    };
    const onKeyUp = (ev) => {
      if (ev.key === key) setIsKeyPressed(false);
    };
    window.addEventListener("keydown", onKeyDown);
    window.addEventListener("keyup", onKeyUp);

    return () => {
      window.removeEventListener("keyup", onKeyUp);
      window.removeEventListener("keydown", onKeyDown);
    };
  }, [key]);

  return isKeyPressed;
}
Enter fullscreen mode Exit fullscreen mode

We ensure that the isKeyPressed value is set to false in case the key parameter has changed by calling setIsKeyPressed at the start of our hook. In case the setState (setIsKeyPressed) function is called with the same value as the state (isKeyPressed), this will not trigger any unnecessary re-renders, because it is strict-equal, which is cool as we don't need to add any "if-statement" noise πŸ‘Œ.

After using this hook in production for some time I experienced a bug which was reported by a Windows user:

I used the hook for tracking whether the Alt key is pressed. On Windows you can tab between windows with the key shortcut Alt + Tab.

This combination resulted in the isKeyPressed value being updated to true, but not back to false as the keyup event was not triggered on the window object.

After he pressed the Alt key again when switching back to the browser window, everything worked again.

I did some research on how to solve this issue and first thought about setting up an interval timer that checks whether a key is still pressed after some time.

While doing that research I also learned that there is no API for checking whether a key is pressed or not. It is only possible by setting up listeners for the keydown and keyup events πŸ˜”.

So my next idea was to somehow detect when the browser window is unfocused. I found some crazy methods like running requestAnimationFrame and checking whether the delay between calls is around one second as it is throttled when not focused.

Fortunately, there is a simpler solution which just requires us to setup one more event listener, the blur event.

It seems like the blur event is fired on the window element when minimizing the window, pushing it to the background or any similar action.

We can adjust our hook to just set the isKeyPressed value back to false upon blur.

import { useState, useEffect } from "react";

export const useIsKeyPressed = (key) => {
  const [isKeyPressed, setIsKeyPressed] = useState(false);

  useEffect(() => {
    setIsKeyPressed(false)
    const onKeyDown = (ev) => {
      if (ev.key === key) setIsKeyPressed(true);
    };
    const onKeyUp = (ev) => {
      if (ev.key === key) setIsKeyPressed(false);
    };
    const onBlur = () => {
      setIsKeyPressed(false);
    };

    window.addEventListener("keydown", onKeyDown);
    window.addEventListener("keyup", onKeyUp);
    window.addEventListener("blur", onBlur);

    return () => {
      window.removeEventListener("keyup", onKeyUp);
      window.removeEventListener("keydown", onKeyDown);
      window.removeEventListener("blur", onBlur);
    };
  }, [key]);

  return isPressed;
};
Enter fullscreen mode Exit fullscreen mode

Another thing I realized quickly afterward is that a keyup event is not triggered in case you press the cmd (Meta) key in addition the any other key on MacOS.

The workaround for this is always set isKeyPressed to false when the keyup key is the Meta key. This will result in a true -> false -> true state change, which is not optimal but still better than being stuck in the isKeyPressed true and having to press the key again to leave it.

I am curious if there are other methods to prevent this, let me know your thoughts in the comments below ⬇️.

Let's take a look at our final hook:

import React from "react";

const useIsKeyPressed = (key) => {
  const [isKeyPressed, setIsKeyPressed] = React.useState(false);

  React.useEffect(() => {
    setIsKeyPressed(false);
    const onKeyDown = (ev) => {
      if (ev.key === key) setIsKeyPressed(true);
    };
    const onKeyUp = (ev) => {
      if (ev.key === key || ev.key === "Meta") setIsKeyPressed(false);
    };
    const onBlur = (ev) => {
      setIsKeyPressed(false);
    };
    window.addEventListener("keydown", onKeyDown);
    window.addEventListener("keyup", onKeyUp);
    window.addEventListener("blur", onBlur);

    return () => {
      window.removeEventListener("keyup", onKeyUp);
      window.removeEventListener("keydown", onKeyDown);
      window.removeEventListener("blur", onBlur);
    };
  }, [key]);

  return isKeyPressed;
};

Enter fullscreen mode Exit fullscreen mode

**Demo-Time Bonus πŸŽ‰:

Thank you for reading ☺️

Discussion (0)