Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

react register simultaneous setState (hooks)

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.) screenshot of bug

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.

like image 940
cubefox Avatar asked Feb 07 '26 01:02

cubefox


2 Answers

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

like image 161
cbdeveloper Avatar answered Feb 09 '26 15:02

cbdeveloper


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.

like image 40
cubefox Avatar answered Feb 09 '26 17:02

cubefox