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

IT技术 javascript reactjs react-hooks keyboard-events
2021-05-11 00:45:55

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

这似乎工作正常......

4个回答

有 3 个关键的事情要做才能使它像您的类组件一样按预期工作。

正如其他人提到的,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/

存储库中基本部分,完整的工作组件:

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-depsfor 的原因useEffect是因为一旦pressedorpressedKeys数组发生变化,我不想每次都重新附加事件

我希望这有帮助!

用户@Vencovsky 提到了Gabe RaglanduseKeyPress 配方实现这一点使一切都按预期工作。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>
  )
}

完整的小提琴可以在这里找到

与我的代码的不同之处在于它使用每个键的钩子和状态而不是一次使用所有键。我不确定为什么这很重要。如果有人能解释一下就好了。

感谢所有试图帮助我并使钩子概念更清晰的人。感谢@Vencovsky 将我指向 Gabe Ragland 的 usehooks.com 网站。

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

我找到的所有解决方案都非常糟糕。例如,该线程中的解决方案只允许您按住 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