反应寄存器同时设置状态(钩子)

时间:2019-03-29 09:35:56

标签: javascript reactjs react-hooks

我有一个组件可以监听多个按键事件。

 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);
    };
  });

是核心。整个代码可以找到here (codesandbox)

它在90%的时间内都能正常工作,但是:

在完全相同的时间释放2个键时,不会两次更新状态。

(您可以尝试在上面提供的codeandbox中重现它,同时按任意2个键,然后在完全相同的时间释放它们。可能需要尝试几次才能正确使用,here is a gif of the issue happening,这就是它的外观例如(数字是按下的键的数量,“ s”是应该按下的键,但是,您可以看到在控制台日志中注册了“ s”的键。) screenshot of bug

有人曾经遇到过类似的事情吗? 从我可以看到的问题不是

  • 事件侦听器(console.log被触发)

  • 重新渲染(您可以尝试按其他一些键,该键将保留在数组中)

这就是为什么我假设问题出在钩子上,但是我无法解释为什么会这样。

3 个答案:

答案 0 :(得分:2)

问题是您使用的是setState(newValue)的简单形式,并且用新值替换了状态。您必须使用setState( (prevState) => {} );的功能形式,因为您的新状态取决于先前的状态。

尝试一下:

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

答案 1 :(得分:1)

原来我只需要将useEffect替换为useLayoutEffect

来自docs

  

签名与useEffect相同,但是在所有DOM突变后都会同步触发。使用它从DOM读取布局并同步重新渲染。在浏览器有机会绘制之前,useLayoutEffect内部安排的更新将被同步刷新。

答案 2 :(得分:0)

我已经测试了您的代码,发现它不仅仅是2个键,实际上,您可以使用更多的键更轻松地重现它;尝试同时按下asdjkl(6个键),您会看到更多的混乱情况。

我认为问题在于,react的重渲染周期不适用于dom事件侦听器,这意味着onKeyDownonKeyUp会在需要时触发,但react不会在每次触发时重新渲染。只需登录[...activeKeys, e.key][...activeKeys].filter(i => i !== e.key),您就会看到它。

我认为解决方案是使用一个简单的局部变量来更新activeKeys,然后render方法应使用此变量而不是钩子的结果,最后在每个{ {1}}和onKeyDown使反应重新呈现。