我正在尝试为我正在开发的游戏创建基于箭头的键盘控件。当然,我试图与React保持同步,所以我想创建一个功能组件并使用钩子。我为越野车部件创建了JSFiddle。
几乎可以按预期工作,除了我同时按下许多箭头键时。然后似乎没有触发某些keyup
事件。也可能是“状态”未正确更新。
我喜欢这样:
const ALLOWED_KEYS = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']
const [pressed, setPressed] = React.useState([])
const handleKeyDown = React.useCallback(event => {
const { key } = event
if (ALLOWED_KEYS.includes(key) && !pressed.includes(key)) {
setPressed([...pressed, key])
}
}, [pressed])
const handleKeyUp = React.useCallback(event => {
const { key } = event
setPressed(pressed.filter(k => k !== key))
}, [pressed])
React.useEffect(() => {
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('keyup', handleKeyUp)
return () => {
document.removeEventListener('keydown', handleKeyDown)
document.removeEventListener('keyup', handleKeyUp)
}
})
我的想法是我做对了,但是对钩子不熟悉,这很可能就是问题所在。特别是由于我已经重新创建了与基于类的组件相同的组件:https://jsfiddle.net/vus4nrfe/
这似乎很好用...
我相信您是Breaking the Rules of Hooks:
请勿在传递给
useMemo
,useReducer
或useEffect
的函数中调用Hooks。
[您正在调用传递给setPressed
的函数中的useCallback
钩子,该函数基本上在后台使用useMemo
。
[
useCallback(fn, deps)
等效于useMemo(() => fn, deps)
。
https://reactjs.org/docs/hooks-reference.html#usecallback
[看看是否通过使用普通箭头功能删除useCallback
是否可以解决您的问题。
useEffect
在每个渲染上运行,导致在每个按键上添加/删除您的侦听器。这可能会导致在没有连接侦听器的情况下按下/释放键。
将空数组[]
作为第二个参数提供给useEffect
,React将知道此效果不依赖于任何props / state值,因此它永远不需要重新运行,附加和清理监听器
React.useEffect(() => {
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('keyup', handleKeyUp)
return () => {
document.removeEventListener('keydown', handleKeyDown)
document.removeEventListener('keyup', handleKeyUp)
}
}, [])
React.useEffect(() => {
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('keyup', handleKeyUp)
return () => {
document.removeEventListener('keydown', handleKeyDown)
document.removeEventListener('keyup', handleKeyUp)
}
}, [handleKeyDown, handleKeyUp]); // <---- Add this deps array
您需要将处理程序作为依赖项添加到useEffect
,否则将在每个渲染器上调用它。
此外,请确保您的deps数组不为空[]
,因为您的处理程序可能会基于pressed
的值进行更改。
用户@Vencovsky提到了Gabe Ragland的useKeyPress recipe。实施此操作可使一切正常工作。 useKeyPress配方:
// Hook
const useKeyPress = (targetKey) => {
// State for keeping track of whether key is pressed
const [keyPressed, setKeyPressed] = React.useState(false)
// If pressed key is our target key then set to true
const downHandler = ({ key }) => {
if (key === targetKey) {
setKeyPressed(true)
}
}
// If released key is our target key then set to false
const upHandler = ({ key }) => {
if (key === targetKey) {
setKeyPressed(false)
}
}
// Add event listeners
React.useEffect(() => {
window.addEventListener('keydown', downHandler)
window.addEventListener('keyup', upHandler)
// Remove event listeners on cleanup
return () => {
window.removeEventListener('keydown', downHandler)
window.removeEventListener('keyup', upHandler)
}
}, []) // Empty array ensures that effect is only run on mount and unmount
return keyPressed
}
然后您可以按如下方式使用该“挂钩”:
const KeyboardControls = () => {
const isUpPressed = useKeyPress('ArrowUp')
const isDownPressed = useKeyPress('ArrowDown')
const isLeftPressed = useKeyPress('ArrowLeft')
const isRightPressed = useKeyPress('ArrowRight')
return (
<div className="keyboard-controls">
<div className={classNames('up-button', isUpPressed && 'pressed')} />
<div className={classNames('down-button', isDownPressed && 'pressed')} />
<div className={classNames('left-button', isLeftPressed && 'pressed')} />
<div className={classNames('right-button', isRightPressed && 'pressed')} />
</div>
)
}
可以找到完整的小提琴here。
与我的代码的区别在于,它使用钩子和状态每个键而不是一次使用所有键。我不确定为什么这会很重要。如果有人可以解释的话,那就太好了。
感谢所有试图帮助我并使钩子概念更清晰的人。感谢@Vencovsky给我指向Gabe Ragland的usehooks.com网站。
要使它像预期的类组件一样工作,需要做三件事。
正如针对useEffect
的其他提到的那样,您需要添加[]
作为依赖项数组,该数组仅在addEventLister
运行时才触发。
第二个主要问题是,您没有像在类组件中那样在功能组件中对pressed
数组的先前状态进行了突变,如下所示:
// onKeyDown event
this.setState(prevState => ({
pressed: [...prevState.pressed, key],
}))
// onKeyUp event
this.setState(prevState => ({
pressed: prevState.pressed.filter(k => k !== key),
}))
您需要在功能之一中进行以下更新:
// onKeyDown event
setPressedKeys(previousPressedKeys => [...previousPressedKeys, key]);
// onKeyUp event
setPressedKeys(previousPressedKeys => previousPressedKeys.filter(k => k !== key));
第三件事是onKeyDown
和onKeyUp
事件的定义已移至useEffect
内部,因此您无需使用useCallback
。
提到的事情最终解决了我的问题。请找到我所做的以下工作GitHub存储库,它们可以按预期工作:
https://github.com/norbitrial/react-keydown-useeffect-componentdidmount
如果您更喜欢这里,请找到工作的JSFiddle版本:
来自存储库的essential part,完全正常工作的组件:
const KeyDownFunctional = () => {
const [pressedKeys, setPressedKeys] = useState([]);
useEffect(() => {
const onKeyDown = ({key}) => {
if (Consts.ALLOWED_KEYS.includes(key) && !pressedKeys.includes(key)) {
setPressedKeys(previousPressedKeys => [...previousPressedKeys, key]);
}
}
const onKeyUp = ({key}) => {
if (Consts.ALLOWED_KEYS.includes(key)) {
setPressedKeys(previousPressedKeys => previousPressedKeys.filter(k => k !== key));
}
}
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp);
return () => {
document.removeEventListener('keydown', onKeyDown);
document.removeEventListener('keyup', onKeyUp);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <>
<h3>KeyDown Functional Component</h3>
<h4>Pressed Keys:</h4>
{pressedKeys.map(e => <span key={e} className="key">{e}</span>)}
</>
}
我之所以将// eslint-disable-next-line react-hooks/exhaustive-deps
用作useEffect
的原因是,当pressed
或pressedKeys
数组更改后,我不想每次都重新附加事件。
我希望这会有所帮助!