-
-
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; | |
} |
Prettier arbitrarily modifies the code on save by replacing []
with [downHandler, upHandler]
:
// 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);
};
},
[downHandler, upHandler]
); // Empty array ensures that effect is only run on mount and unmount
This results in a warning in the console:
./src/hooks/useKeyPress.js
Line 8: The 'downHandler' function makes the dependencies of useEffect Hook (at line 33) change on every render. Move it inside the useEffect callback. Alternatively, wrap the 'downHandler' definition into its own useCallback() Hook react-hooks/exhaustive-deps
Line 15: The 'upHandler' function makes the dependencies of useEffect Hook (at line 33) change on every render. Move it inside the useEffect callback. Alternatively, wrap the 'upHandler' definition into its own useCallback() Hook react-hooks/exhaustive-deps
This can be solved by moving the two functions inside useEffect
:
// Add event listeners
useEffect(
() => {
// 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);
}
};
window.addEventListener("keydown", downHandler);
window.addEventListener("keyup", upHandler);
// Remove event listeners on cleanup
return () => {
window.removeEventListener("keydown", downHandler);
window.removeEventListener("keyup", upHandler);
};
},
[targetKey]
);
Now the console becomes clear.
Please note the key codes are following the event.key
notation instead of the older but widely used event.keyCode
.
Thus Enter will become Enter
instead of 13
, or the right arrow ArrowRight
instead of 39
Source: https://stackoverflow.com/questions/27827234/how-to-handle-the-onkeypress-event-in-reactjs
(Old) Key codes: https://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes
I adapted a version of this hook to use in my own app.
I added optional event handlers that you could use in your component to fire onPressDown & onPressUp:
import { useEffect, useState } from 'react';
export function useKeyPress(targetKey, onPressDown = () => {}, onPressUp = () => {}) {
// State for keeping track of whether key is pressed
const [keyPressed, setKeyPressed] = useState(false);
useEffect(() => {
// If pressed key is our target key then set to true
function downHandler({ key }) {
if (key === targetKey) {
setKeyPressed(true);
onPressDown();
}
}
// If released key is our target key then set to false
const upHandler = ({ key }) => {
if (key === targetKey) {
setKeyPressed(false);
onPressUp();
}
};
// Add event listeners
window.addEventListener('keydown', downHandler);
window.addEventListener('keyup', upHandler);
// Remove event listeners on cleanup
return () => {
window.removeEventListener('keydown', downHandler);
window.removeEventListener('keyup', upHandler);
};
});
return keyPressed;
}
Usage:
const escapePressed = useKeyPress('Escape', onPressDown, onPressUp);
I like that implementation @e2o - thanks for sharing all!
I don't really have a suggestion but couldn't find another way to thank you for creating the useHooks project. It's proven extremely valuable to me and even taught me some vanilla JS stuff I didn't know since it rarely comes up. Thanks!
@sunny-mittal Glad you've found it valuable!
I just needed a function to be fired when a key was pressed, so I made something like this:
export const useKeyPress = (targetKey, fn) => {
function downHandler({ key }) {
if (key === targetKey) {
fn();
}
}
useEffect(() => {
window.addEventListener('keydown', downHandler);
return () => {
window.removeEventListener('keydown', downHandler);
};
}, []);
};
// ...
const Modal = () => {
useKeyPress('Escape', () => {
console.log('exit modal');
});
return ...
}
I've run into issues with all of these approaches when trying to detect if a key has been pressed.
The earlier versions used a keyup
handler to change the pressed state when the key is released, but this is not ideal when I just want the effect to happen once. In addition, it can cause an infinite loop when the result of the keypress changes the state of the component and causes a re-render. On the re-render, the hook will be called again, the keypress will still be set, and the re-render will happen again, etc.
The later approaches above use a callback in the hook. This works quite well, but it's not ideal because the callback should really be provided as a dependency of the useEffect
call, since if it changes you need to change re-apply the listener. This means that unless you also memoize the callback (using useCallback
or useMemo
), then the event listener will be added and removed very frequently. Even memoizing the callback is problematic for the same reasons; the callback likely modifies or reads some state and therefore may need to be changed any time its own dependencies change, in turn triggering a change on the listener.
The version below allows for a single read of the keypress only. The second time that the hook is called, we erase the keypress. This may not be ideal for all uses, but I thought it might benefit someone who got stuck with infinite loops as I did. This version uses event.keyCode
but could easily be modified for event.key
.
function useKeyCode(keyCode) {
const [isKeyPressed, setKeyPressed] = useState();
// Only allow fetching each keypress event once to prevent infinite loops
if (isKeyPressed) {
setKeyPressed(false);
}
useEffect(() => {
function downHandler(event) {
if (event.keyCode === keyCode) {
setKeyPressed(true);
}
}
window.addEventListener('keydown', downHandler);
return () => window.removeEventListener('keydown', downHandler);
}, [keyCode]);
return isKeyPressed;
}
Here's how you'd use it:
function List({items}) {
const [highlightedIndex, setHighlightedIndex] = useState(0);
const moveDown = useKeyCode(40);
const moveUp = useKeyCode(38);
if (moveDown) {
setHighlightedIndex(prev => clamp(prev + 1, 0, items.length - 1));
}
if (moveUp) {
setHighlightedIndex(prev => clamp(prev - 1, 0));
}
return <ul>{items.map((item, index) => <li key={item.id} className={highlightedIndex === index ? 'highlighted' : ''}>{item.text}</li>)}</ul>;
}
Or as a full example: https://codepen.io/sirbrillig/pen/jONLvYy
Question: the way I see it, every hook is run every time the component renders, no? That would be bad for performance when instead you could delegate events and register one common event handler only at start with
function App() {
useEffect(() => {
window.addEventListener("keydown", myHandlerForEverything)
return () => window.removeEventListener("keydown", myHandlerForEverything)
}, [])
return <lalalala />
}
using keyCode instead of key
import { useState, useEffect } from 'react';
// CHECK: https://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes
export default function useKeyPress(targetKeyCode) {
const [keyPressed, setKeyPressed] = useState(false);
useEffect(() => {
let prevKeyCode = '';
function downHandler({ keyCode }) {
if (prevKeyCode === targetKeyCode) return;
if (keyCode === targetKeyCode) {
setKeyPressed(true);
prevKeyCode = keyCode;
}
}
function upHandler({ keyCode }) {
if (keyCode === targetKeyCode) {
setKeyPressed(false);
prevKeyCode = '';
}
}
window.addEventListener('keydown', downHandler);
window.addEventListener('keyup', upHandler);
return () => {
window.removeEventListener('keydown', downHandler);
window.removeEventListener('keyup', upHandler);
};
}, [targetKeyCode]);
return keyPressed;
}
How would you go about unit testing this custom hook?
I would probably try to mock the window
object and maybe use @testing-library/react-hooks
too // cc @ j-mcgregor
import { useState, useEffect } from "react";
export function useKeyPressed(keyLookup: (event: KeyboardEvent) => boolean) {
const [keyPressed, setKeyPressed] = useState(false);
useEffect(() => {
const downHandler = (ev: KeyboardEvent) => setKeyPressed(keyLookup(ev));
const upHandler = (ev: KeyboardEvent) => setKeyPressed(keyLookup(ev));
window.addEventListener("keydown", downHandler);
window.addEventListener("keyup", upHandler);
return () => {
window.removeEventListener("keydown", downHandler);
window.removeEventListener("keyup", upHandler);
};
}, [keyLookup]);
return keyPressed;
}
This one (written in TypeScript) allows to listen to key combinations such as CMD+Enter or CTRL+Enter:
const shouldSubmit = useKeyPressed(
(ev: KeyboardEvent) => (ev.metaKey || ev.ctrlKey) && ev.key === "Enter"
);
if (shouldSubmit) {
// Do something
}
Greetings. Made a simple adaption for my needs here.
In my case, keypress acting as a toggle for sidebar appearence.
Important part here is:
if (document.activeElement.nodeName !== 'INPUT') { toogle... }
Otherwise my sidebar kept toggleling upon user input anywhere in the page =)
export 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 (document.activeElement.nodeName !== 'INPUT') {
if (key === targetKey) {
setKeyPressed(keyPressed => !keyPressed);
}
}
}
// Add event listeners
useEffect(() => {
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;
}
I noticed that the demo of the useMultiKeyPress which was done by @jhsu
no longer seems recognise multi press events in the latest version of react. I recreated the pen using react 17.0.0.
useMultiKeyPress: React 17.0.0
useMultiKeyPress: React 16.7.0-alpha.0 - next
I can also confirm that this does not work correctly on windows 10
EDIT: It is partly due to the keyboard that I am using also :)
EDIT: I Just stumbled across this article which explains the limitations ok the keyboard and why you will have issues with some key combinations.
With multiple keys pressed:
function testUseKeyPress() {
const onPressSingle = () => {
console.log('onPressSingle!')
}
const onPressMulti = () => {
console.log('onPressMulti!')
}
useKeyPress('a', onPressSingle)
useKeyPress('shift h', onPressMulti)
}
export default function useKeyPress(keys, onPress) {
keys = keys.split(' ').map((key) => key.toLowerCase())
const isSingleKey = keys.length === 1
const pressedKeys = useRef([])
const keyIsRequested = (key) => {
key = key.toLowerCase()
return keys.includes(key)
}
const addPressedKey = (key) => {
key = key.toLowerCase()
const update = pressedKeys.current.slice()
update.push(key)
pressedKeys.current = update
}
const removePressedKey = (key) => {
key = key.toLowerCase()
let update = pressedKeys.current.slice()
const index = update.findIndex((sKey) => sKey === key)
update = update.slice(0, index)
pressedKeys.current = update
}
const downHandler = ({ key }) => {
const isKeyRequested = keyIsRequested(key)
if (isKeyRequested) {
addPressedKey(key)
}
}
const upHandler = ({ key }) => {
const isKeyRequested = keyIsRequested(key)
if (isKeyRequested) {
if (isSingleKey) {
pressedKeys.current = []
onPress()
} else {
const containsAll = keys.every((i) => pressedKeys.current.includes(i))
removePressedKey(key)
if (containsAll) {
onPress()
}
}
}
}
useEffect(() => {
window.addEventListener('keydown', downHandler)
window.addEventListener('keyup', upHandler)
return () => {
window.removeEventListener('keydown', downHandler)
window.removeEventListener('keyup', upHandler)
}
}, [])
}
@tenjojeremy thanks for this really similar to what I have done. Take a look!!
useAllKeysPress
Demos
1. Single key
2. Key on focused element
3. Multiple keys
4. Multiple keys in order
@Numel2020 how do you deal with all the rerenders?
@tenjojeremy thanks for this really similar to what I have done. Take a look!!
useAllKeysPress
Demos
1. Single key
2. Key on focused element
3. Multiple keys
4. Multiple keys in order
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 the Meta
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 like const isComboPress = useKeyCombo("Meta+k");
to capture Command+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]);
};
Codesanbox is not working
Hey, can someone help me out. I want to implement traversal through inputs (which are basically in table cells in a Table) using Arrow Keys (Up, Down, Left and Right). I am struggling to find a solution to do this. I have searched through Google But All the Solutions I have found are done using JQuery. My Goal is to Tackle this problem in React. I have tried this all the hooks present here. But none worked out for me. Maybe, I couldn't get it's logic because I am still a beginner.
@Suryakaran1234
Cool. This seems like a nice challenge.
What I would try is something like this:
- Define a matrix-like identification with two integers. Maybe you already have it. I mean something like this:
let table = [
{row: 1, column: 1, content: "excel like cell 1A"},
{row: 1, column: 2, content: "excel like cell 1B"},
{row: 1, column: 3, content: "excel like cell 1C"},
{row: 2, column: 1, content: "excel like cell 2A"},
{row: 2, column: 2, content: "excel like cell 2B"},
{row: 2, column: 3, content: "excel like cell 2C"},
] // and so on
- You may have a variable that gives what is your current active cell.
let active = [1, 3] //row and column, so it would point to 1C as active.
- Define a few functions like so:
function moveDown(active)
let [row, column] = active //destructure array to have access to row and column
// you will need some error boundaries if your table has a limit on the number of cells, say 50 rows, for example.
if (row === 50) {
return active //does nothing because there is not a cell below it to go to
}
return [row + 1, column]
function moveUp(active)
let [row, column] = active //destructure array to have access to row and column
if (row === 1) {
return active //does nothing because there is not a cell above it
}
return [row - 1, column]
function moveLeft(active)
let [row, column] = active //destructure array to have access to row and column
if (column === 1) {
return active //does nothing because there is not a cell to the left
}
return [row, column - 1]
function moveRight(active)
let [row, column] = active //destructure array to have access to row and column
if (column === 50) {
return active //does nothing beacuse column 50 is our table limit.
}
return [row, column + 1]
- Pack these functions into an object to abstract it. We know that keyCodes for arrows are:
37 --> left
38 --> up
39 --> right
40 --> down
So our hook function needs to detect a keypress, check if the keyCode is any of these values from 37 to 40 and call the event.
let actions = {
37: moveLeft,
38: moveUp,
39: moveRight,
40: moveDown,
}
function useTraversalThroughInputs(initial) {
const [active, setActive] = useState(initial) //active is our default value, the initial selected cell
// keyboard key has been pressed
function downHandler(e) {
if ([37, 38, 39, 40].includes(e.keyCode)) {
let action = actions[e.keyCode] // this gives us the handler. say.. e.keyCode is 39, it returns moveLeft
let newActiveCell = action(active) // receives the [1,1] and returns [1,2]
setActive(newActiveCell) // active cell is now [1,2] and our hook returns it to our functional component
}
}
// Add event listeners
useEffect(() => {
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 active
}
Now our hook gives us the current cell position. We could go further and incorporate into the hook the ability to select the element and use input.focus() to make it active. But I will keep concerns separate here for clarity.
function Table() {
let current = useTraversalThroughInputs(initial) // where initial is the default active cell
useEffect(() => {
// select the input element. depends on the structure of your table.
let element = document.getElementBy...
element.focus()
// useEffect you be triggered every time active has been changed, and will change focus to your new active cell.
}, [active])
)
return (
/// your table content with some sort of identification for selecting cells based on [row, column].
)
Thank you so Much for this, I will be implementing it right away and let you know if I face any issues😊.
@felipe-dap
Hey, I tried your solution but it's a little more complicated for me and my code just became more complex. Can you help me out with this, I am struggling pretty hard to implement this.
This is My Component
Here is my component (I am using Material UI v5) 👇🏻
`import * as PropTypes from 'prop-types';
import styled from '@emotion/styled';
import { css } from '@emotion/react';
import { Grid } from '@mui/material';
import useTraversalThroughInputs from './useTraversalThroughInputs';
import { useEffect } from 'react';
const styles = csswidth: 100%; padding: 0 10px; border: 1px solid black; font-size: 14px; font-weight: bold; margin: 5px 0 0 0; height: 25px; border-radius: 2px;
;
const titleStyles = csswidth: 20%; height: 5vh; text-align: center; padding: 5px 25px; font-style: italic; font-size: 1rem; color: #fff;
;
const StyledOpen = styled.td${titleStyles}; background-color: #ce4848;
;
const StyledJodi = styled.td${titleStyles}; color: #000; background-color: #37e180;
;
const StyledClose = styled.td${titleStyles}; background-color: #000;
;
const StyledInput = styled.input${styles}
;
const StyledOpenInput = styled.inputbackground-color: #ce4848; ${styles}
;
const StyledCloseInput = styled.inputcolor: #fff; background-color: #000; ${styles}
;
function Jodi({ jodiArr = [] }) {
// eslint-disable-next-line prefer-const
let active = [1, 1]; // row and column, so it would point to 1C as active.
const current = useTraversalThroughInputs(active);
const jodi = [...jodiArr];
const row = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
const column = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
useEffect(() => {
// select the input element. depends on the structure of your table.
const tableBody = document.querySelector('#table-body ');
tableBody.focus();
// useEffect you be triggered every time active has been changed, and will change focus to your new active cell.
}, [active]);
const openClose = jodi.slice(0, 10);
console.log(jodi);
return (
<>
<Grid container spacing={1} component="table" direction="column" justifyContent="space-between">
<thead>
<Grid item component="tr" sx={{ display: 'flex', justifyContent: 'space-between' }}>
<StyledOpen>Open</StyledOpen>
<StyledJodi>Jodi</StyledJodi>
<StyledClose>Close</StyledClose>
</Grid>
</thead>
<tbody id="table-body">
{openClose.map((open, index) => {
const jodis = jodi.splice(0, 10);
// eslint-disable-next-line prefer-const
let indexJodi = index * 10;
return (
<tr key={open} id={row[index]}>
<Grid item xs={1} md={1} xl={1} component="td">
<label htmlFor={`OPEN_${open}`}>{open}</label>
<StyledOpenInput type="text" id={column[index]} name={`OPEN_${open}`} />
</Grid>
{jodis.map((jodi, indexJ) => (
<Grid item key={indexJodi + indexJ} xs={1} md={1} xl={1} component="td">
<label htmlFor={`Jodi_${jodi}`}>{jodi}</label>
<StyledInput type="text" id={column[index]} name={`Jodi_${jodi}`} />
</Grid>
))}
<Grid item key={open + 2} xs={1} md={1} xl={1} component="td">
<label htmlFor={`CLOSE_${open}`}>{open}</label>
<StyledCloseInput type="text" id={column[index]} name={`CLOSE_${open}`} />
</Grid>
</tr>
);
})}
</tbody>
</Grid>
</>
);
}
Jodi.propTypes = {
jodiArr: PropTypes.array
};
export default Jodi;
`
Here is My Array that I am passing to this component👇🏻
`const jodiArr = [
"00", "01", "02", "03", "04", "05", "06", "07", "08", "09",
"10", "11", "12", "13", "14", "15", "16", "17", "18", "19",
"20", "21", "22", "23", "24", "25", "26", "27", "28", "29",
"30", "31", "32", "33", "34", "35", "36", "37", "38", "39",
"40", "41", "42", "43", "44", "45", "46", "47", "48", "49",
"50", "51", "52", "53", "54", "55", "56", "57", "58", "59",
"60", "61", "62", "63", "64", "65", "66", "67", "68", "69",
"70", "71", "72", "73", "74", "75", "76", "77", "78", "79",
"80", "81", "82", "83", "84", "85", "86", "87", "88", "89",
"90", "91", "92", "93", "94", "95", "96", "97", "98", "99"];
export default jodiArr;
`
@Suryakaran1234
Sure I can help you out on this. =)
Do you mind if I make a few refactorings along the way? Mostly for organization pourposes...
This is getting way off topic, so I made a repo in order for not cluttering this thread anymore.
Probably tomorrow there should be a demo on there. Find me there.
https://github.com/felipe-dap/useTraversalThroughInputs
and we can chat at
felipe-dap/useTraversalThroughInputs#1
Cheers.
@felipe-dap
Yeah of course go ahead, it will be helpful for me if you do refactorings, I will get to learn more.
function testUseKeyPress() { const onPressSingle = () => { console.log('onPressSingle!') } const onPressMulti = () => { console.log('onPressMulti!') } useKeyPress('a', onPressSingle) useKeyPress('shift h', onPressMulti) }
onKeyPressed
Thanks @jeremytenjo
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);
}
})
To make the hook more robust it should be updated to rerun the effect if the
targetKey
changes.here is an updated gist
and here is a codesandbox example