-
-
Save gragland/b61b8f46114edbcf2a9e4bd5eb9f47f5 to your computer and use it in GitHub Desktop.
import { useState, useEffect } from 'react'; | |
// Usage | |
function App() { | |
// Call our hook for each key that we'd like to monitor | |
const happyPress = useKeyPress('h'); | |
const sadPress = useKeyPress('s'); | |
const robotPress = useKeyPress('r'); | |
const foxPress = useKeyPress('f'); | |
return ( | |
<div> | |
<div>h, s, r, f</div> | |
<div> | |
{happyPress && '😊'} | |
{sadPress && '😢'} | |
{robotPress && '🤖'} | |
{foxPress && '🦊'} | |
</div> | |
</div> | |
); | |
} | |
// Hook | |
function useKeyPress(targetKey) { | |
// State for keeping track of whether key is pressed | |
const [keyPressed, setKeyPressed] = useState(false); | |
// If pressed key is our target key then set to true | |
function 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 | |
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; | |
} |
For folks attempting to capture
Meta
key pressed in combination with other keys (Command+k
e.g. on Macs), be warned that keyup events do not fire when theMeta
key is still pressed. That means that this hook cannot be used reliably to detect when keys are unpressed. Read issue #3 here: https://web.archive.org/web/20160304022453/http://bitspushedaround.com/on-a-few-things-you-may-not-know-about-the-hellish-command-key-and-javascript-events/To work around this, I do not rely on keyup events at all but instead "unpress" automatically after a second. It's a bit hacky but serves my use case quite well (Command+k):
export function useKeyPress(targetKey: string) { // State for keeping track of whether key is pressed const [keyPressed, setKeyPressed] = useState<boolean>(false); // Add event listeners useEffect(() => { // If pressed key is our target key then set to true function downHandler({ key }: any) { if (!keyPressed && key === targetKey) { setKeyPressed(true); // rather than rely on keyup to unpress, use a timeout to workaround the fact that // keyup events are unreliable when the meta key is down. See Issue #3: // http://web.archive.org/web/20160304022453/http://bitspushedaround.com/on-a-few-things-you-may-not-know-about-the-hellish-command-key-and-javascript-events/ setTimeout(() => { setKeyPressed(false); }, 1000); } } window.addEventListener("keydown", downHandler); // Remove event listeners on cleanup return () => { window.removeEventListener("keydown", downHandler); }; }, []); // Empty array ensures that effect is only run on mount and unmount return keyPressed; }And if anyone is interested, here's my tiny
useKeyCombo
extension that can be used likeconst isComboPress = useKeyCombo("Meta+k");
to captureCommand+k
:export const useKeyCombo = (keyCombo: string) => { const keys = keyCombo.split("+"); const keyPresses = keys.map((key) => useKeyPress(key)); return keyPresses.every(keyPressed => keyPressed === true); };Lastly, I almost always just want to trigger some logic when these key conditions are met, so I made a wrapper hook that does that for me and allows for usage like this:
useOnKeyPressed("Meta+k", () => setIsQuickSearchOpen(true));
:export const useOnKeyPressed = (keyCombo: string, onKeyPressed: () => void) => { const isKeyComboPressed = useKeyCombo(keyCombo); useEffect(() => { if (isKeyComboPressed) { onKeyPressed(); } }, [isKeyComboPressed]); };
have encountourred any problem while using this?
I would suggest adding 'blur' event to the window. I'm using this hook to see if 'Shift' is being pressed, but when I pressed 'Shift' and at same time go to another window (e.g devtools or another tab), the state wasn't being set as false, and when I went back to my tab the state was still true (since I release 'Shift' in another tab).
// ...
const setAsNotBeingPressed = useCallback(() => {
setKeyPressed(false);
}, []);
const setAsBeingPressed = useCallback(() => {
setKeyPressed(true);
}, []);
useEffet(() => {
// ....
window.addEventListener("blur", setAsBeingPressed);
return () => {
// ...
window.removeEventListener("blur", setAsNotBeingPressed);
}
})
Thanks @jeremytenjo