Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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;
}
@awnigharbia

This comment has been minimized.

Copy link

commented Nov 16, 2018

import React, { useState, useEffect } from "react";

export default function useKeyPress(targetKey) {
  // State for keeping track of whether key is pressed
  const [keyPressed, setKeyPressed] = useState(false);
  let prevKey = "";

  // If pressed key is our target key then set to true
  function downHandler({ key }) {
    // checking keep pressing re-rendering
    if (prevKey === targetKey) return;

    if (key === targetKey) {
      setKeyPressed(true);
      prevKey = key;
    }
  }

  // If released key is our target key then set to false
  const upHandler = ({ key }) => {
    if (key === targetKey) {
      setKeyPressed(false);
      prevKey = "";
    }
  };
  // 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;
}

I suggest checking the long pressing for the key itself so it prevents it from re-reding.

@tikotzky

This comment has been minimized.

Copy link

commented Feb 13, 2019

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

@metamn

This comment has been minimized.

Copy link

commented May 26, 2019

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.

@metamn

This comment has been minimized.

Copy link

commented May 26, 2019

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

@e2o

This comment has been minimized.

Copy link

commented Jun 6, 2019

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);
@sethdavis512

This comment has been minimized.

Copy link

commented Jun 24, 2019

I like that implementation @e2o - thanks for sharing all!

@sunny-mittal

This comment has been minimized.

Copy link

commented Jul 20, 2019

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!

@gragland

This comment has been minimized.

Copy link
Owner Author

commented Jul 20, 2019

@sunny-mittal Glad you've found it valuable!

@MiLeung

This comment has been minimized.

Copy link

commented Jul 25, 2019

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 ...
}
@sirbrillig

This comment has been minimized.

Copy link

commented Sep 1, 2019

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.