September 4, 2021
6 min read
Dealing with keyboard events in React can be tricky to figure out at first. I had a project recently where I wanted to trigger some code in response to a user's key presses. I ended up responding to their events with a combination of useEffect
and document.addEventListener
in a useKeyPress
custom hook.
Skip to the finished useKeyPress
custom hook code or continue reading to learn how it was built.
keypress
event in browsersThere are keydown
events that are triggered continuously while a given key is pressed down. This means just attaching an event handler to keydown
is not enough for a piece of code you only want triggered a single time.
const onKeyDown = (event) => { thisFunctionGetsInvoked_LOTS_perKeyDownEvent();};
document.addEventListener("keydown", onKeyDown);
Fortunately, in newer browsers keyboard events come with a repeat
property. For the first keydown
event triggered by a single key this property will be false. For every subsequent event before the user releases the key, repeat
will be true.
const onKeyDown = (event) => { if (!event.repeat) { thisFunctionGetsInvoked_ONCE_perKeyDownEvent(); }};
document.addEventListener("keydown", onKeyDown);
Now we've got a keydown
event listener that can trigger logic once in response to a user pressing a key. Next, we'll need to get this working inside of React.
useEffect
hookThe best way I've found to attach a global event listener in React is via the useEffect
hook. In this code sample below I abstract this into a custom hook called useKeyPress
.
const useKeyPress = () => { useEffect(() => { const onKeyDown = (event) => { if (!event.repeat) { thisFunctionGetsInvoked_ONCE_perKeyDownEvent(); } };
document.addEventListener("keydown", onKeyDown); }, []);};
This application of useEffect
has a few issues we'll cover next.
The first issue is our lack of clean up function being returned from our effect function. We should use document.removeEventListener
for this purpose. This will prevent duplicate event handlers being attached when this hook is disposed.
const useKeyPress = () => { useEffect(() => { const onKeyDown = (event) => { if (!event.repeat) { thisFunctionGetsInvoked_ONCE_perKeyDownEvent(); } };
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown); }, []);};
Currently, our event handler will trigger when any key is pressed. We want to only respond to specific key presses. The KeyboardEvent
object has a code
property to help us figure out which key was pressed. These codes are strings like "KeyA"
and "Space"
.
I decided to have this hook allow for multiple different key handlers. It does this by receiving an object of key codes mapped to functions. When a given key code event is sent, if the handlers
object contains the code we invoke it.
We also want to pass this handlers
object as a dependency to our useEffect
hook. This ensures our event handlers are kept up to date if our user changes them.
const useKeyPress = (handlers = {}) => { useEffect(() => { const onKeyDown = (event) => { if (!event.repeat) { handlers[event.code]?.(); } };
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown); }, [handlers]);};
Finally, this hook will accept array of dependencies from the user of the hook and spread them into the useEffect
hook. This way passed in event handlers can use the latest state of their parent component.
useKeyPress
custom hookconst useKeyPress = (handlers = {}, dependencies = []) => { useEffect(() => { const onKeyDown = (event) => { if (!event.repeat) { handlers[event.code]?.(); } };
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown); }, [handlers, ...dependencies]);};
// Log some console messages when the A and Space keys are placedconst Component = () => { useKeyPress({ KeyA: () => console.log("I pressed the A key!"), Space: () => console.log("I pressed the Space key!"), });
return null;};
// Increment and display a count when Space is pressedconst ComponentWithState = () => { const [count, setCount] = useState(0);
useKeyPress( { Space: () => setCount(count + 1), }, [count] );
return <p>{count}</p>;};