I'm trying to create arrow based keyboard controls for a game I'm working on. Of course I'm trying to stay up to date with React so I wanted to create a function component and use hooks. I've created a JSFiddle for my buggy component.
It's almost working as expected, except when I press a lot of the arrow keys at the same time. Then it seems like some keyup
events aren't triggered. It could also be that the 'state' is not updated properly.
Which I do like this:
const ALLOWED_KEYS = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']
const [pressed, setPressed] = React.useState([])
const handleKeyDown = React.useCallback(event => {
const { key } = event
if (ALLOWED_KEYS.includes(key) && !pressed.includes(key)) {
setPressed([...pressed, key])
}
}, [pressed])
const handleKeyUp = React.useCallback(event => {
const { key } = event
setPressed(pressed.filter(k => k !== key))
}, [pressed])
React.useEffect(() => {
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('keyup', handleKeyUp)
return () => {
document.removeEventListener('keydown', handleKeyDown)
document.removeEventListener('keyup', handleKeyUp)
}
})
I have the idea that I'm doing it correctly, but being new to hooks it is very likely that this is where the problem is. Especially since I've re-created the same component as a class based component: https://jsfiddle.net/vus4nrfe/
And that seems to work fine...
To handle key presses in React, we use ‘onKeyPress’. It is passed as an attribute in <input> elements, and can be used to perform actions for any event involving the keyboard, whether you want to call a function on any key press, or only when a specific key is pressed.
Hooks are a feature in React that allow you use state and other React features without writing classes. This website provides easy to understand code examples to help you learn how hooks work and inspire you to take advantage of them in your next project. Your weekly dose of JavaScript news. Delivered every monday to 101,495 devs, for free.
Events such as a click of a mouse button, scrolling, a key press, or a drag of a component—to mention but a few—help developers capture specific actions from users and show feedback or take action based on the action of the user. In this guide, you'll focus solely on keyboard events and how to handle them in React.
We’ll use modern React features including hooks and functional components. You won’t see old-fashioned stuff like class components or things related to them. There are 3 keyboard events:
There are 3 key things to do to make it work as expected just like your class component.
As others mentioned for useEffect
you need to add an []
as a dependency array which will trigger only once the addEventLister
functions.
The second thing which is the main issue is that you are not mutating the pressed
array's previous state in functional component as you did in class component, just like below:
// onKeyDown event
this.setState(prevState => ({
pressed: [...prevState.pressed, key],
}))
// onKeyUp event
this.setState(prevState => ({
pressed: prevState.pressed.filter(k => k !== key),
}))
You need to update in functional one as the following:
// onKeyDown event
setPressedKeys(previousPressedKeys => [...previousPressedKeys, key]);
// onKeyUp event
setPressedKeys(previousPressedKeys => previousPressedKeys.filter(k => k !== key));
The third thing is that the definition of the onKeyDown
and onKeyUp
events have been moved inside of useEffect
so you don't need to use useCallback
.
The mentioned things solved the issue on my end. Please find the following working GitHub repository what I've made which works as expected:
https://github.com/norbitrial/react-keydown-useeffect-componentdidmount
Find a working JSFiddle version if you like it better here:
https://jsfiddle.net/0aogqbyp/
The essential part from the repository, fully working component:
const KeyDownFunctional = () => {
const [pressedKeys, setPressedKeys] = useState([]);
useEffect(() => {
const onKeyDown = ({key}) => {
if (Consts.ALLOWED_KEYS.includes(key) && !pressedKeys.includes(key)) {
setPressedKeys(previousPressedKeys => [...previousPressedKeys, key]);
}
}
const onKeyUp = ({key}) => {
if (Consts.ALLOWED_KEYS.includes(key)) {
setPressedKeys(previousPressedKeys => previousPressedKeys.filter(k => k !== key));
}
}
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp);
return () => {
document.removeEventListener('keydown', onKeyDown);
document.removeEventListener('keyup', onKeyUp);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <>
<h3>KeyDown Functional Component</h3>
<h4>Pressed Keys:</h4>
{pressedKeys.map(e => <span key={e} className="key">{e}</span>)}
</>
}
The reason why I'm using // eslint-disable-next-line react-hooks/exhaustive-deps
for the useEffect
is because I don't want to reattach the events every single time once the pressed
or pressedKeys
array is changing.
I hope this helps!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With