Keydown/up 个 React Hooks 无法正常工作的事件
Keydown/up events with React Hooks not working properly
我正在尝试为我正在开发的游戏创建基于箭头的键盘控件。当然,我正在尝试与 React 保持同步,所以我想创建一个函数组件并使用钩子。我为我的错误组件创建了一个 JSFiddle。
它几乎按预期工作,除非我同时按下很多箭头键。然后似乎没有触发某些 keyup
事件。也可能是 'state' 没有正确更新。
我喜欢这样:
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)
}
})
我认为我做的是正确的,但是作为 hooks 的新手,很可能这就是问题所在。特别是因为我重新创建了与基于 class 的组件相同的组件:
https://jsfiddle.net/vus4nrfe/
这似乎工作正常...
useEffect
运行 每次渲染,导致 adding/removing 您的听众每次按键。这可能会导致密钥 press/release 没有附加侦听器。
提供一个空数组 []
作为 useEffect
的第二个参数,React 将知道此效果不依赖于任何 props/state 值,因此它永远不需要重新运行,附加并清理您的侦听器一次
React.useEffect(() => {
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('keyup', handleKeyUp)
return () => {
document.removeEventListener('keydown', handleKeyDown)
document.removeEventListener('keyup', handleKeyUp)
}
}, [])
我相信你是 Breaking the Rules of Hooks:
Do not call Hooks inside functions passed to useMemo
, useReducer
, or useEffect
.
您在传递给 useCallback
的函数中调用 setPressed
挂钩,它基本上在幕后使用 useMemo
。
useCallback(fn, deps)
is equivalent to useMemo(() => fn, deps)
.
https://reactjs.org/docs/hooks-reference.html#usecallback
看看删除 useCallback
以支持普通箭头函数是否可以解决您的问题。
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
的值发生变化。
要使它像您的 class 组件一样按预期工作,需要做 3 件关键的事情。
正如其他人针对 useEffect
提到的那样,您需要添加一个 []
作为依赖数组,它只会在 addEventLister
函数执行一次时触发。
第二个主要问题是您没有像在 class 中那样改变功能组件中 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>)}
</>
}
我对 useEffect
使用 // eslint-disable-next-line react-hooks/exhaustive-deps
的原因是因为我不想在 pressed
或 [=24= 之后每次都重新附加事件] 数组正在改变。
希望对您有所帮助!
用户@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
}
然后您可以按如下方式使用 "hook":
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>
)
}
完整fiddle可以找到here.
我的代码的不同之处在于它使用挂钩和状态 每个键 而不是一次使用所有键。我不确定为什么这很重要。如果有人能解释一下那就太好了。
感谢所有尝试提供帮助并让我更清楚 hooks 概念的人。感谢@Vencovsky 将我指向 Gabe Ragland 的 usehooks.com 网站。
我找到的所有解决方案都很糟糕。例如,此线程中的解决方案仅允许您按住 2 个按钮,或者它们根本无法像许多 use-hooks 库那样工作。
在与来自#Reactiflux 的@asafaviv 合作了很长时间之后,我认为这是我最喜欢的解决方案:
import { useState, useLayoutEffect } from 'react'
const specialKeys = [
`Shift`,
`CapsLock`,
`Meta`,
`Control`,
`Alt`,
`Tab`,
`Backspace`,
`Escape`,
]
const useKeys = () => {
if (typeof window === `undefined`) return [] // Bail on SSR
const [keys, setKeys] = useState([])
useLayoutEffect(() => {
const downHandler = ({ key, shiftKey, repeat }) => {
if (repeat) return // Bail if they're holding down a key
setKeys(prevKeys => {
return [...prevKeys, { key, shiftKey }]
})
}
const upHandler = ({ key, shiftKey }) => {
setKeys(prevKeys => {
return prevKeys.filter(k => {
if (specialKeys.includes(key))
return false // Special keys being held down/let go of in certain orders would cause keys to get stuck in state
return JSON.stringify(k) !== JSON.stringify({ key, shiftKey }) // JS Objects are unique even if they have the same contents, this forces them to actually compare based on their contents
})
})
}
window.addEventListener(`keydown`, downHandler)
window.addEventListener(`keyup`, upHandler)
return () => {
// Cleanup our window listeners if the component goes away
window.removeEventListener(`keydown`, downHandler)
window.removeEventListener(`keyup`, upHandler)
}
}, [])
return keys.map(x => x.key) // return a clean array of characters (including special characters )
}
export default useKeys
我正在尝试为我正在开发的游戏创建基于箭头的键盘控件。当然,我正在尝试与 React 保持同步,所以我想创建一个函数组件并使用钩子。我为我的错误组件创建了一个 JSFiddle。
它几乎按预期工作,除非我同时按下很多箭头键。然后似乎没有触发某些 keyup
事件。也可能是 'state' 没有正确更新。
我喜欢这样:
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)
}
})
我认为我做的是正确的,但是作为 hooks 的新手,很可能这就是问题所在。特别是因为我重新创建了与基于 class 的组件相同的组件: https://jsfiddle.net/vus4nrfe/
这似乎工作正常...
useEffect
运行 每次渲染,导致 adding/removing 您的听众每次按键。这可能会导致密钥 press/release 没有附加侦听器。
提供一个空数组 []
作为 useEffect
的第二个参数,React 将知道此效果不依赖于任何 props/state 值,因此它永远不需要重新运行,附加并清理您的侦听器一次
React.useEffect(() => {
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('keyup', handleKeyUp)
return () => {
document.removeEventListener('keydown', handleKeyDown)
document.removeEventListener('keyup', handleKeyUp)
}
}, [])
我相信你是 Breaking the Rules of Hooks:
Do not call Hooks inside functions passed to
useMemo
,useReducer
, oruseEffect
.
您在传递给 useCallback
的函数中调用 setPressed
挂钩,它基本上在幕后使用 useMemo
。
useCallback(fn, deps)
is equivalent touseMemo(() => fn, deps)
.
https://reactjs.org/docs/hooks-reference.html#usecallback
看看删除 useCallback
以支持普通箭头函数是否可以解决您的问题。
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
的值发生变化。
要使它像您的 class 组件一样按预期工作,需要做 3 件关键的事情。
正如其他人针对 useEffect
提到的那样,您需要添加一个 []
作为依赖数组,它只会在 addEventLister
函数执行一次时触发。
第二个主要问题是您没有像在 class 中那样改变功能组件中 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>)}
</>
}
我对 useEffect
使用 // eslint-disable-next-line react-hooks/exhaustive-deps
的原因是因为我不想在 pressed
或 [=24= 之后每次都重新附加事件] 数组正在改变。
希望对您有所帮助!
用户@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
}
然后您可以按如下方式使用 "hook":
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>
)
}
完整fiddle可以找到here.
我的代码的不同之处在于它使用挂钩和状态 每个键 而不是一次使用所有键。我不确定为什么这很重要。如果有人能解释一下那就太好了。
感谢所有尝试提供帮助并让我更清楚 hooks 概念的人。感谢@Vencovsky 将我指向 Gabe Ragland 的 usehooks.com 网站。
我找到的所有解决方案都很糟糕。例如,此线程中的解决方案仅允许您按住 2 个按钮,或者它们根本无法像许多 use-hooks 库那样工作。
在与来自#Reactiflux 的@asafaviv 合作了很长时间之后,我认为这是我最喜欢的解决方案:
import { useState, useLayoutEffect } from 'react'
const specialKeys = [
`Shift`,
`CapsLock`,
`Meta`,
`Control`,
`Alt`,
`Tab`,
`Backspace`,
`Escape`,
]
const useKeys = () => {
if (typeof window === `undefined`) return [] // Bail on SSR
const [keys, setKeys] = useState([])
useLayoutEffect(() => {
const downHandler = ({ key, shiftKey, repeat }) => {
if (repeat) return // Bail if they're holding down a key
setKeys(prevKeys => {
return [...prevKeys, { key, shiftKey }]
})
}
const upHandler = ({ key, shiftKey }) => {
setKeys(prevKeys => {
return prevKeys.filter(k => {
if (specialKeys.includes(key))
return false // Special keys being held down/let go of in certain orders would cause keys to get stuck in state
return JSON.stringify(k) !== JSON.stringify({ key, shiftKey }) // JS Objects are unique even if they have the same contents, this forces them to actually compare based on their contents
})
})
}
window.addEventListener(`keydown`, downHandler)
window.addEventListener(`keyup`, upHandler)
return () => {
// Cleanup our window listeners if the component goes away
window.removeEventListener(`keydown`, downHandler)
window.removeEventListener(`keyup`, upHandler)
}
}, [])
return keys.map(x => x.key) // return a clean array of characters (including special characters )
}
export default useKeys