web-dev-qa-db-ja.com

Reactを使用したキーダウン/アップイベントが正しく機能しない

作業中のゲームの矢印ベースのキーボードコントロールを作成しようとしています。もちろん、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/

そしてそれはうまくいくようです...

2
thomasjonas

クラスコンポーネントと同じように期待どおりに機能させるには、3つの重要な点があります。

他の人がuseEffectについて述べたように、addEventLister関数を1回だけトリガーする依存関係配列として[]を追加する必要があります。

主な問題である2番目のことは、クラスで行ったように、機能コンポーネントで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));

3つ目は、onKeyDownおよびonKeyUpイベントの定義が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>)}
    </>
}

useEffect// eslint-disable-next-line react-hooks/exhaustive-depsを使用している理由は、pressedまたはpressedKeys配列を1回作成するたびにイベントを再接続したくないためです。変わってきている。

これが役に立てば幸いです!

1
norbitrial

フックのルールを破る

useMemouseReducer、またはuseEffectに渡された関数内でフックを呼び出さないでください。

setPressedに渡された関数内でuseCallbackフックを呼び出しています。これは、基本的には内部でuseMemoを使用します。

useCallback(fn, deps)useMemo(() => fn, deps)と同等です。

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

単純な矢印関数を優先してuseCallbackを削除すると、問題が解決するかどうかを確認します。

1
skoller

私が見つけたすべての解決策はかなり悪かった。たとえば、このスレッドのソリューションでは、2つのボタンを押し続けることしかできません。

#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
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

ハンドラーを依存関係としてuseEffectに追加する必要があります。そうしないと、すべてのレンダリングでハンドラーが呼び出されます。

また、deps配列が空でないことを確認してください[]、ハンドラーはpressedの値に基づいて変更される可能性があるためです。

1
Laith

useEffectはすべてのレンダリングで実行され、キーを押すたびにリスナーが追加/削除されます。これにより、リスナーが接続されていない状態でキーが押されたり離されたりする可能性があります。

空の配列を追加する[]useEffectの2番目のパラメータとして、Reactは、この効果がどのprops/state値にも依存しないため、再実行、アタッチ、およびリスナーを一度クリーンアップする

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

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

ユーザー@Vencovskyは、Gabe Raglandによる seKeyPressレシピ について言及しました。これを実装すると、すべてが期待どおりに機能しました。 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>
  )
}

完全なフィドルを見つけることができます ここ

私のコードとの違いは、すべてのキーを一度に使用するのではなく、フックと状態per keyを使用することです。なぜそれが問題になるのかはわかりません。誰かがそれを説明できたら素晴らしいでしょう。

助けようとして、フックのコンセプトを明確にしてくれた皆さんに感謝します。そして、@ VencovskyがGabe Raglandのusehooks.com Webサイトを紹介してくれてありがとう。

0
thomasjonas