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));

第三件事是 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>)}
    </>
}

我对 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