web-dev-qa-db-fra.com

Événements keydown / up avec React Les hooks ne fonctionnent pas correctement

J'essaie de créer des commandes de clavier basées sur des flèches pour un jeu sur lequel je travaille. Bien sûr, j'essaie de rester à jour avec React donc je voulais créer un composant de fonction et utiliser des hooks. J'ai créé un JSFiddle pour mon buggy composant.

Cela fonctionne presque comme prévu, sauf lorsque j'appuie sur de nombreuses touches fléchées en même temps. Ensuite, il semble que certains événements keyup ne sont pas déclenchés. Il se peut également que "l'état" ne soit pas mis à jour correctement.

Ce que je fais comme ça:

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

J'ai l'idée que je le fais correctement, mais étant nouveau dans les hooks, il est très probable que c'est là que se trouve le problème. Surtout depuis que j'ai recréé le même composant qu'un composant basé sur une classe: https://jsfiddle.net/vus4nrfe/

Et cela semble bien fonctionner ...

2
thomasjonas

Il y a 3 choses clés à faire pour que cela fonctionne comme prévu, tout comme votre composant de classe.

Comme d'autres l'ont mentionné pour useEffect, vous devez ajouter un [] comme un tableau de dépendances qui ne déclenchera qu'une seule fois les fonctions addEventLister.

La deuxième chose qui est le problème principal est que vous ne mutez pas l'état précédent du tableau pressed dans le composant fonctionnel comme vous l'avez fait en classe composant, comme ci-dessous:

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

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

Vous devez mettre à jour dans un fonctionnel comme suit:

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

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

La troisième chose est que la définition des événements onKeyDown et onKeyUp a été déplacée à l'intérieur de useEffect donc vous n'avez pas besoin d'utiliser useCallback.

Les choses mentionnées ont résolu le problème de mon côté. Veuillez trouver le référentiel GitHub fonctionnel suivant ce que j'ai fait et qui fonctionne comme prévu:

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

Trouvez une version JSFiddle fonctionnelle si vous l'aimez mieux ici:

https://jsfiddle.net/0aogqbyp/

Le partie essentielle du référentiel, composant entièrement fonctionnel:

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

La raison pour laquelle j'utilise // eslint-disable-next-line react-hooks/exhaustive-deps pour le useEffect est parce que je ne veux pas rattacher les événements à chaque fois une fois que le tableau pressed ou pressedKeys change.

J'espère que ça aide!

1
norbitrial

Je crois que vous êtes Briser les règles des crochets :

N'appelez pas les Hooks à l'intérieur des fonctions transmises à useMemo, useReducer ou useEffect.

Vous appelez le crochet setPressed dans une fonction passée à useCallback, qui utilise essentiellement useMemo sous le capot.

useCallback(fn, deps) équivaut à useMemo(() => fn, deps).

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

Voyez si la suppression de useCallback en faveur d'une fonction de flèche simple résout votre problème.

1
skoller

Toutes les solutions que j'ai trouvées étaient plutôt mauvaises. Par exemple, les solutions de ce fil vous permettent uniquement de maintenir 2 boutons enfoncés, ou elles ne fonctionnent tout simplement pas comme la plupart des bibliothèques use-hooks.

Après avoir longtemps travaillé dessus avec @asafaviv de #Reactiflux, je pense que c'est ma solution préférée:

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
1
corysimmons
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

Vous devez ajouter les gestionnaires en tant que dépendances au useEffect, sinon il est appelé à chaque rendu.

Assurez-vous également que votre tableau deps n'est pas vide [], car vos gestionnaires peuvent changer en fonction de la valeur de pressed.

1
Laith

useEffect s'exécute sur chaque rendu, résultant en l'ajout/la suppression de vos écouteurs à chaque pression de touche. Cela pourrait conduire à une pression/relâchement de touche sans qu'un auditeur ne soit attaché.

Remplissage d'un tableau vide [] comme deuxième paramètre de useEffect, React saura que cet effet ne dépend d'aucune des valeurs d'accessoires/d'état, il n'a donc jamais besoin de réexécuter, d'attacher et de nettoyer vos auditeurs une fois

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

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

L'utilisateur @Vencovsky a mentionné la recette useKeyPress de Gabe Ragland. La mise en œuvre de cela a tout fonctionné comme prévu. La recette 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
}

Vous pouvez ensuite utiliser ce "hook" comme suit:

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

Le violon complet peut être trouvé ici .

La différence avec mon code est qu'il utilise des crochets et l'état par clé au lieu de toutes les clés à la fois. Je ne sais pas pourquoi cela importerait. Ce serait formidable si quelqu'un pouvait expliquer cela.

Merci à tous ceux qui ont essayé de m'aider et qui ont clarifié le concept des crochets pour moi. Et merci pour @Vencovsky de m'avoir indiqué le site Web usehooks.com de Gabe Ragland.

0
thomasjonas