Description
Do you want to request a feature or report a bug?
Report a bug.
What is the current behavior?
I have an app that's registering event listeners for window
's key events (via useEffect
). Those event listeners are triggering state updates (via useState
). I think I have found a bug where simultaneous key events occurring in the same frame (whether down or up) will be handled out of order, causing state to becoming out of sync.
Take the following simple app (https://codesandbox.io/s/1z3v9zrk4j). I've kept this as keyup only for simplicity.
function App() {
const [keys, setKeys] = useState([]);
console.log('App', keys);
const onKeyUp = function (event) {
console.log('onKeyUp', event.key, keys);
setKeys([...keys, event.key]);
};
useEffect(function () {
console.log('effect', keys);
window.addEventListener('keyup', onKeyUp);
return function () {
console.log('removing event listener', keys);
window.removeEventListener('keyup', onKeyUp);
};
});
return <p>{keys.join(', ')}</p>;
}
If I press down any two keys, e.g. the "q" and "w" keys, and then release them at precisely the same time, the following happens:
- The
keyup
event listener forw
is called, which in turn callssetKeys
with['w']
App
is re-rendered withkeys === ['w']
- The
keyup
event listener forq
is called, which in turn callssetKeys
with['q']
- The effect's cleanup function is called, removing the event listener with
keys === []
- The effect is run again, the event listener being added with
keys === ['w']
App
is re-rendered withkeys === ['q']
- The effect's cleanup function is called, removing the event listener with
keys ===['w']
- The effect is run again, the event listener being added with
keys === ['q']
This results in keys === ['q']
. The render with w
has been lost.
With three keys, only two keys are reliably shown. Four keys - only two are reliably shown.
If I add another useState
call, the first useState
has no issues - all keys are reliably detected. See https://codesandbox.io/s/0yo51n5wv:
function App() {
const [keys, setKeys] = useState([]);
const [dummy, setDummy] = useState('foo');
console.log("rendering App", keys);
const onKeyUp = function(event) {
console.log("onKeyUp event received", event.key, keys);
setKeys([...keys, event.key]);
setDummy('foo');
};
useEffect(function() {
console.log("adding event listener", keys);
window.addEventListener("keyup", onKeyUp);
return function() {
console.log("removing event listener", keys);
window.removeEventListener("keyup", onKeyUp);
};
});
return (
<div>
<p>Keyups received:</p>
<p>{keys.join(", ")}</p>
<button onClick={() => setKeys([])}>Reset</button>
</div>
);
}
What is the expected behavior?
I would expect the final state array to contain all keys released, in order. There are a few workarounds for this issue (e.g. passing a function to setState
to retrieve the current value instead of using the rendered value), but from the documentation it seems that is an escape hatch for use when the effect's callback is not renewed on each state change, and should not be necessary in this case (unless I've misunderstood).
Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
It happens on both versions that support hooks - 16.8.0-alpha.0
and 16.8.0-alpha.1
. This is on Chrome/Safari/Firefox on MacOS Mojave.