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} />
}
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;
}
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;
};
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;
};
**Demo-Time Bonus ๐:
Thank you for reading โบ๏ธ
Top comments (0)