Skip to content

Instantly share code, notes, and snippets.

@gragland
Created November 14, 2018 18:21
Show Gist options
  • Save gragland/b61b8f46114edbcf2a9e4bd5eb9f47f5 to your computer and use it in GitHub Desktop.
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;
}
@eli7gn
Copy link

eli7gn commented Jul 25, 2022

function testUseKeyPress() {
 const onPressSingle = () => {
    console.log('onPressSingle!')
  }
  const onPressMulti = () => {
    console.log('onPressMulti!')
  }

  useKeyPress('a', onPressSingle)
  useKeyPress('shift h', onPressMulti)
}
onKeyPressed

Thanks @jeremytenjo

@Chandraprakash-Darji
Copy link

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

have encountourred any problem while using this?

@alexrintt
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment