Homebrew React Hooks: useIsKeyPressed
Tags: webdev, react, beginners, javascript
Published: 2020-04-03 06:22:36

**[Cover Art](https://www.pexels.com/photo/brown-silver-and-black-oscar-fish-2109800/)** by **[Enrique Grisales](https://www.pexels.com/@enrique-grisales-911754)**


**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`](https://developer.mozilla.org/en-US/docs/Web/API/Document/keydown_event) and [`keyup`](https://developer.mozilla.org/en-US/docs/Web/API/Document/keyup_event) events for our event handlers.

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

```js
import React from "react";

const MyComponent = () => {
  const handler = () => {
    console.log("hey")
  }
  return <div onKeyDown={handler} />
}
```

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`](https://developer.mozilla.org/en-US/docs/Web/API/Window) object.

**Let's build our hook:**

```js
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;
}
```

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`](https://developer.mozilla.org/en-US/docs/Web/API/Window/blur_event) 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`.

```js
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;
};
```

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:

```js
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;
};

```

**Demo-Time Bonus 🎉:

{% codesandbox t1mjj %}

Thank you for reading ☺️