Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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. I've created a JSFiddle for my buggy component.

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. 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/

And that seems to work fine...

like image 405
thomasjonas Avatar asked Dec 31 '19 17:12

thomasjonas


People also ask

How to handle key presses in react?

To handle key presses in React, we use ‘onKeyPress’. It is passed as an attribute in <input> elements, and can be used to perform actions for any event involving the keyboard, whether you want to call a function on any key press, or only when a specific key is pressed.

What are react hooks?

Hooks are a feature in React that allow you use state and other React features without writing classes. This website provides easy to understand code examples to help you learn how hooks work and inspire you to take advantage of them in your next project. Your weekly dose of JavaScript news. Delivered every monday to 101,495 devs, for free.

What are keyboard events in react?

Events such as a click of a mouse button, scrolling, a key press, or a drag of a component—to mention but a few—help developers capture specific actions from users and show feedback or take action based on the action of the user. In this guide, you'll focus solely on keyboard events and how to handle them in React.

What features will we use in ReactJS?

We’ll use modern React features including hooks and functional components. You won’t see old-fashioned stuff like class components or things related to them. There are 3 keyboard events:


1 Answers

There are 3 key things to do to make it work as expected just like your class component.

As others mentioned for useEffect you need to add an [] as a dependency array which will trigger only once the addEventLister functions.

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:

// 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.

The mentioned things solved the issue on my end. Please find the following working GitHub repository what I've made which works as expected:

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

Find a working JSFiddle version if you like it better here:

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.

I hope this helps!

like image 148
norbitrial Avatar answered Oct 06 '22 00:10

norbitrial