简体   繁体   English

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

[英]Keydown/up events with React Hooks not working properly

I'm trying to create arrow based keyboard controls for a game I'm working on.我正在尝试为我正在开发的游戏创建基于箭头的键盘控件。 Of course I'm trying to stay up to date with React so I wanted to create a function component and use hooks.当然,我正在努力与 React 保持同步,所以我想创建一个函数组件并使用钩子。 I've created a JSFiddle for my buggy component.我为我的错误组件创建了一个JSFiddle

It's almost working as expected, except when I press a lot of the arrow keys at the same time.它几乎按预期工作,除非我同时按下很多箭头键。 Then it seems like some keyup events aren't triggered.然后似乎没有触发某些keyup事件。 It could also be that the 'state' is not updated properly.也可能是“状态”未正确更新。

Which I do like this:我喜欢这样:

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

I have the idea that I'm doing it correctly, but being new to hooks it is very likely that this is where the problem is.我的想法是我做对了,但是对于钩子来说是新手,很可能这就是问题所在。 Especially since I've re-created the same component as a class based component: https://jsfiddle.net/vus4nrfe/特别是因为我重新创建了与基于类的组件相同的组件: https : //jsfiddle.net/vus4nrfe/

And that seems to work fine...这似乎工作正常......

There are 3 key things to do to make it work as expected just like your class component.有 3 个关键的事情要做才能使它像您的类组件一样按预期工作。

As others mentioned for useEffect you need to add an [] as a dependency array which will trigger only once the addEventLister functions.正如其他人提到的useEffect你需要添加一个[]作为依赖数组,它只会触发一次addEventLister函数。

The second thing which is the main issue is that you are not mutating the pressed array's previous state in functional component as you did in class component, just like below:第二个主要问题是您没有像在类组件中那样改变功能组件中pressed数组的先前状态,如下所示:

// onKeyDown event
this.setState(prevState => ({
   pressed: [...prevState.pressed, key],
}))

// onKeyUp event
this.setState(prevState => ({
   pressed: prevState.pressed.filter(k => k !== key),
}))

You need to update in functional one as the following:您需要更新功能一如下:

// onKeyDown event
setPressedKeys(previousPressedKeys => [...previousPressedKeys, key]);

// onKeyUp event
setPressedKeys(previousPressedKeys => previousPressedKeys.filter(k => k !== key));

The third thing is that the definition of the onKeyDown and onKeyUp events have been moved inside of useEffect so you don't need to use useCallback .第三件事是onKeyDownonKeyUp事件的定义已移到useEffect内部,因此您不需要使用useCallback

The mentioned things solved the issue on my end.提到的事情解决了我的问题。 Please find the following working GitHub repository what I've made which works as expected:请找到我制作的以下工作 GitHub 存储库,它按预期工作:

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

Find a working JSFiddle version if you like it better here:如果您更喜欢,请在此处找到可用的 JSFiddle 版本:

https://jsfiddle.net/0aogqbyp/ https://jsfiddle.net/0aogqbyp/

The essential part from the repository, fully working component:存储库中的基本部分,完全工作的组件:

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>)}
    </>
}

The reason why I'm using // eslint-disable-next-line react-hooks/exhaustive-deps for the useEffect is because I don't want to reattach the events every single time once the pressed or pressedKeys array is changing.为什么我使用的原因// eslint-disable-next-line react-hooks/exhaustive-depsuseEffect是因为我不想重新连接的事件,每一次,一旦pressedpressedKeys阵列正在发生变化。

I hope this helps!我希望这有帮助!

User @Vencovsky mentioned the useKeyPress recipe by Gabe Ragland.用户@Vencovsky 提到了 Gabe Ragland 的useKeyPress 配方 Implementing this made everything work as expected.实现这一点使一切都按预期工作。 The 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
}

You can then use that "hook" as follows:然后,您可以按如下方式使用该“挂钩”:

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

Complete fiddle can be found here .完整的小提琴可以在这里找到。

The difference with my code is that it use hooks and state per key instead of all the keys at once.与我的代码的不同之处在于它使用每个键的钩子和状态而不是一次使用所有键。 I'm not sure why that would matter though.我不确定为什么这很重要。 Would be great if somebody could explain that.如果有人能解释一下就好了。

Thanks to everyone who tried to help and made the hooks concept clearer for me.感谢所有试图帮助我并使钩子概念更清晰的人。 And thanks for @Vencovsky for pointing me to the usehooks.com website by Gabe Ragland.感谢@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

You need to add the handlers as dependencies to the useEffect , otherwise it gets called on every render.您需要将处理程序作为依赖项添加到useEffect ,否则在每次渲染时都会调用它。

Also, make sure your deps array is not empty [] , because your handlers could change based on the value of pressed .此外,请确保您的DEPS数组不为空[]因为你的处理器可能会更改基于价值pressed

All the solutions I found were pretty bad.我找到的所有解决方案都非常糟糕。 For instance, the solutions in this thread only allow you to hold down 2 buttons, or they simply don't work like a lot of the use-hooks libraries.例如,这个线程中的解决方案只允许你按住 2 个按钮,或者它们根本不像很多 use-hooks 库那样工作。

After working on this for a long time with @asafaviv from #Reactiflux I think this is my favorite solution:在与来自#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

useEffect runs on every render, resulting on adding/removing your listeners on each keypress. useEffect在每次渲染时运行,导致在每次按键时添加/删除您的侦听器。 This could potential lead to a key press/release without a listener attached.这可能会导致在没有连接侦听器的情况下按下/释放按键。

Suppling an empty array [] as second parameter to useEffect , React will know that this effect does not depend on any of the props/state values so it never needs to re-run, attaching and cleaning up your listeners once提供一个空数组[]作为useEffect第二个参数,React 会知道这个效果不依赖于任何 props/state 值,所以它永远不需要重新运行、附加和清理你的监听器一次

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

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

I believe you're Breaking the Rules of Hooks :我相信你正在打破 Hooks 的规则

Do not call Hooks inside functions passed to useMemo , useReducer , or useEffect .不要在传递给useMemouseReduceruseEffect函数内部调用 Hook。

You're calling the setPressed hook inside a function passed to useCallback , which basically uses useMemo under the hood.您在传递给useCallback的函数中调用setPressed钩子,该函数基本上在useCallback使用useMemo

useCallback(fn, deps) is equivalent to useMemo(() => fn, deps) . useCallback(fn, deps)等价于useMemo(() => fn, deps)

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

See if removing the useCallback in favor of a plain arrow function solves your problem.看看删除useCallback以支持普通箭头函数是否可以解决您的问题。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM