我有一个组件可以监听多个按键事件。
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”的键。)
有人曾经遇到过类似的事情吗? 从我可以看到的问题不是
事件侦听器(console.log被触发)
重新渲染(您可以尝试按其他一些键,该键将保留在数组中)
这就是为什么我假设问题出在钩子上,但是我无法解释为什么会这样。
答案 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)
);
};
答案 1 :(得分:1)
原来我只需要将useEffect
替换为useLayoutEffect
来自docs:
签名与useEffect相同,但是在所有DOM突变后都会同步触发。使用它从DOM读取布局并同步重新渲染。在浏览器有机会绘制之前,useLayoutEffect内部安排的更新将被同步刷新。
答案 2 :(得分:0)
我已经测试了您的代码,发现它不仅仅是2个键,实际上,您可以使用更多的键更轻松地重现它;尝试同时按下asdjkl(6个键),您会看到更多的混乱情况。
我认为问题在于,react的重渲染周期不适用于dom事件侦听器,这意味着onKeyDown
和onKeyUp
会在需要时触发,但react不会在每次触发时重新渲染。只需登录[...activeKeys, e.key]
和[...activeKeys].filter(i => i !== e.key)
,您就会看到它。
我认为解决方案是使用一个简单的局部变量来更新activeKeys
,然后render
方法应使用此变量而不是钩子的结果,最后在每个{ {1}}和onKeyDown
使反应重新呈现。