I have a component that is supposed to listen for multiple key-down events.
const [activeKeys, setActiveKeys] = useState([]);
const onKeyDown = e => {
if (!activeKeys.includes(e.key)) {
console.log("keydown", e.key);
setActiveKeys([...activeKeys, e.key]);
}
};
const onKeyUp = e => {
console.log("keyup", e.key);
setActiveKeys([...activeKeys].filter(i => i !== e.key));
};
useEffect(() => {
document.addEventListener("keydown", onKeyDown);
document.addEventListener("keyup", onKeyUp);
return () => {
document.removeEventListener("keydown", onKeyDown);
document.removeEventListener("keyup", onKeyUp);
};
});
is at the core. the entire code can be found here (codesandbox)
it works fine 90% of the time, however:
when 2 keys are released at the exact same time the state doesn't get updated twice.
(You can try to reproduce it in the codesandbox provided above, press any 2 keys simultaneously and then release them at the exact same time. Might take a few tries to get right, here is a gif of the issue happening and here's what it looks like (the number is the amount of keys pressed down, and the 's' is the key that's supposedly pressed down, however, you can see that that in the console log the keyup of 's' was registered.)

Has anyone encountered something similar before? From what I can see the issue is not with
the event listener (console.log gets fired)
a rerender (you can try to press some other keys, the key will stay in the array)
that's why I'm assuming that the issue lies with the hook, however I have no way to explain why this is happening.
The problem is that you're using the simple form of setState(newValue) and that replaces your state with your new value. You have to use the functional form of setState( (prevState) => {} ); because your new state depends on the previous state.
Try this:
const onKeyDown = e => {
if (!activeKeys.includes(e.key)) {
console.log("keydown", e.key, activeKeys);
setActiveKeys(prevActiveKeys => [...prevActiveKeys, e.key]);
}
};
const onKeyUp = e => {
console.log("keyup", e.key, activeKeys);
setActiveKeys(prevActiveKeys =>
[...prevActiveKeys].filter(i => i !== e.key)
);
};
Link to Sandbox
Turns out that I just needed to replace useEffect by useLayoutEffect
from the docs:
The signature is identical to useEffect, but it fires synchronously after all DOM mutations. Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.
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