Skip to content

Simultaneous key events in effect handled out of order #14750

Closed
@stuartkeith

Description

@stuartkeith

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 for w is called, which in turn calls setKeys with ['w']
  • App is re-rendered with keys === ['w']
  • The keyup event listener for q is called, which in turn calls setKeys 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 with keys === ['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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions