React Hooks的Keydown / up事件无法正常工作

问题描述 投票:1回答:5

我正在尝试为我正在开发的游戏创建基于箭头的键盘控件。当然,我试图与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/

这似乎很好用...

javascript reactjs react-hooks keyboard-events
5个回答
0
投票

我相信您是Breaking the Rules of Hooks

请勿在传递给useMemouseReduceruseEffect的函数中调用Hooks。

[您正在调用传递给setPressed的函数中的useCallback钩子,该函数基本上在后台使用useMemo

[useCallback(fn, deps)等效于useMemo(() => fn, deps)

https://reactjs.org/docs/hooks-reference.html#usecallback

[看看是否通过使用普通箭头功能删除useCallback是否可以解决您的问题。


0
投票

useEffect在每个渲染上运行,导致在每个按键上添加/删除您的侦听器。这可能会导致在没有连接侦听器的情况下按下/释放键。

将空数组[]作为第二个参数提供给useEffect,React将知道此效果不依赖于任何props / state值,因此它永远不需要重新运行,附加和清理监听器

  React.useEffect(() => {
    document.addEventListener('keydown', handleKeyDown)
    document.addEventListener('keyup', handleKeyUp)

    return () => {
      document.removeEventListener('keydown', handleKeyDown)
      document.removeEventListener('keyup', handleKeyUp)
    }
  }, [])

0
投票
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的值进行更改。


0
投票

用户@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网站。


0
投票

要使它像预期的类组件一样工作,需要做三件事。

正如针对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));

第三件事是onKeyDownonKeyUp事件的定义已移至useEffect内部,因此您无需使用useCallback

提到的事情最终解决了我的问题。请找到我所做的以下工作GitHub存储库,它们可以按预期工作:

https://github.com/norbitrial/react-keydown-useeffect-componentdidmount

如果您更喜欢这里,请找到工作的JSFiddle版本:

https://jsfiddle.net/0aogqbyp/

来自存储库的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的原因是,当pressedpressedKeys数组更改后,我不想每次都重新附加事件。

我希望这会有所帮助!

© www.soinside.com 2019 - 2024. All rights reserved.