作業中のゲームの矢印ベースのキーボードコントロールを作成しようとしています。もちろん、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/
そしてそれはうまくいくようです...
クラスコンポーネントと同じように期待どおりに機能させるには、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バージョンを見つけてください。
必須部分 リポジトリから、完全に機能するコンポーネント:
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回作成するたびにイベントを再接続したくないためです。変わってきている。
これが役に立てば幸いです!
useMemo
、useReducer
、またはuseEffect
に渡された関数内でフックを呼び出さないでください。
setPressed
に渡された関数内でuseCallback
フックを呼び出しています。これは、基本的には内部でuseMemo
を使用します。
useCallback(fn, deps)
はuseMemo(() => fn, deps)
と同等です。
https://reactjs.org/docs/hooks-reference.html#usecallback
単純な矢印関数を優先してuseCallback
を削除すると、問題が解決するかどうかを確認します。
私が見つけたすべての解決策はかなり悪かった。たとえば、このスレッドのソリューションでは、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
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
の値に基づいて変更される可能性があるためです。
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サイトを紹介してくれてありがとう。