-
Notifications
You must be signed in to change notification settings - Fork 47.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Simultaneous key events in effect handled out of order #14750
Comments
I haven’t looked in detail yet but could it be because effects (unlike change pointers to React events, for example) are deferred until after paint? So they close over the stale value for a bit. I kind of think that for discrete events where previous state matters you should always use the updater form or go straight to useReducer. For example two setKeys calls in the same tick wouldn’t work either because of batching. |
Thanks for getting back to me. What's stumped me is the fact that adding a second dummy Most problems I've encountered with stale state in effects have made sense - it's probably a rite of passage to try and optimise an effect by only running it once on mount, or improperly declaring inputs, or batching setState calls, as you point out. But this one doesn't seem intuitive at all. |
Thanks for the report. This led to a pretty long discussion that helped us understand this problem space better. The brief summary is:
To sum up — there’s two actionable things to do here, but neither of them needs to be a breaking change so they don’t have to hold back the release. Thanks for reporting. (Sorry if this is dense. I’m mostly writing up so I don’t forget it myself.) |
Thanks for the comprehensive and interesting write up — I appreciate the deep dive into React's event system, and learnt something about the downsides to event emitters! Thanks again for responding with such depth. In the meantime I'll stick with the setState callback fix. |
I would like to add another (very small) code example that i think is related (but not sure). After the second Though the returned value of the hook is fine obviously.
If i remove the array from
I think this is related to your discussion but not sure. Edit Edit#2
Hope that helps. |
Why is |
Also, you shouldn’t ever need to use |
@stuartkeith This should fix, can you test it? Why should we re-subscribe every render? Isn't it a crazy idea? function App() {
const [keys, setKeys] = useState([]);
console.log("rendering App", keys);
const onKeyUp = function(event) {
console.log("onKeyUp event received", event.key, keys);
setKeys(prevKeys => [...prevKeys, event.key]);
};
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>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement); |
@salvoravida I mentioned this workaround in my first comment, and it's the workaround I've gone with in the meantime. Having said that, I tried my app out on Edge yesterday and even that fix doesn't work there - the Resubscribing each render does seem strange at first, but it solves so many problems with effects ending up with stale state, and the effect ends up more declarative and easy to follow. That's why I raised this as an issue - it seems to be an example of an idiomatic and simple effect that doesn't work as expected. Other than this issue, I've found that if I've ended up with stale state in a subscription-style effect, it's a sign I'm prematurely optimising and overthinking it. It's actually OK to subscribe and unsubscribe each render, it's OK to define many inline functions each render. If I encounter an actual performance problem, only then is it time to start micromanaging everything and writing more performant (but more complex and potentially buggy) hooks. |
Honestly i think you missed some points 2)the whole fix includes one subscription only to keyUp event, that in this case is a not a premature optimization, is the correct way of doing it. 3)using invalidation inputArray on effects is not a premature optmization, is just a normal way of writing code (if we understand closures, and callbacks changes over them) without bugs., |
@salvoravida - anything I'd say to that has already been covered in this thread, so I'll leave it at that. Dan has set out a detailed outline of why the issue occurs and how they're going to fix it, so I don't think it's necessary to respond any further. Let's keep this issue on track! |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contribution. |
Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you! |
This appears fixed in React 18. https://codesandbox.io/s/dazzling-proskuriakova-rn830r?file=/package.json I think it's because of the "Consistent useEffect timing" change described here. Thanks again for this case. |
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 (viauseEffect
). Those event listeners are triggering state updates (viauseState
). 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.
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:
keyup
event listener forw
is called, which in turn callssetKeys
with['w']
App
is re-rendered withkeys === ['w']
keyup
event listener forq
is called, which in turn callssetKeys
with['q']
keys === []
keys === ['w']
App
is re-rendered withkeys === ['q']
keys ===['w']
keys === ['q']
This results in
keys === ['q']
. The render withw
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 firstuseState
has no issues - all keys are reliably detected. See https://codesandbox.io/s/0yo51n5wv: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
and16.8.0-alpha.1
. This is on Chrome/Safari/Firefox on MacOS Mojave.The text was updated successfully, but these errors were encountered: