113

I want to bind to close the active react bootstrap popover on escape press. Here is the code

_handleEscKey: function(event) {
  console.log(event);
  if (event.keyCode == 27) {
    this.state.activePopover.hide();
  }
},

componentWillMount: function() {
  BannerDataStore.addChangeListener(this._onchange);
  document.addEventListener("click", this._handleDocumentClick, false);
  document.addEventListener("keyPress", this._handleEscKey, false);
},

componentWillUnmount: function() {
  BannerDataStore.removeChangeListener(this._onchange);
  document.removeEventListener("click", this._handleDocumentClick, false);
  document.removeEventListener("keyPress", this._handleEscKey, false);
},

But nothing is getting logged in the console when I press any key. I have tried to listen that on window also and with different cases 'keypress', 'keyup', etc but it seems I am doing something wrong.

ggorlen
  • 44,755
  • 7
  • 76
  • 106
alwaysLearn
  • 6,882
  • 7
  • 39
  • 67
  • For what it's worth I've published a keydown lib for React that is meant to make all of this a lot easier: https://github.com/jedverity/react-keydown/ – glortho Oct 20 '15 at 18:53

7 Answers7

75

You should use keydown and not keypress.

Keypress (deprecated) is usually used only for keys that produce a character output as per the docs

Keypress (deprecated)

The keypress event is fired when a key is pressed down and that key normally produces a character value

Keydown

The keydown event is fired when a key is pressed down.

Dhiraj
  • 33,140
  • 10
  • 61
  • 78
63

Just had a similar problem with this myself. I'll use your code to illustrate a fix.

// for other devs who might not know keyCodes
var ESCAPE_KEY = 27;

_handleKeyDown = (event) => {
    switch( event.keyCode ) {
        case ESCAPE_KEY:
            this.state.activePopover.hide();
            break;
        default: 
            break;
    }
},

// componentWillMount deprecated in React 16.3
componentDidMount(){
    BannerDataStore.addChangeListener(this._onchange);
    document.addEventListener("click", this._handleDocumentClick, false);
    document.addEventListener("keydown", this._handleKeyDown);
},


componentWillUnmount() {
    BannerDataStore.removeChangeListener(this._onchange);
    document.removeEventListener("click", this._handleDocumentClick, false);
    document.removeEventListener("keydown", this._handleKeyDown);
},

Since you are using the createClass way of doing things, you do not need to bind to certain methods as this is implicit in each method defined.

There is a working jsfiddle, using the createClass method of React component creation here.

Chris Sullivan
  • 1,011
  • 9
  • 11
  • 9
    This will not properly remove the event listener due to the bind giving a new instance each time. Make sure you cache the results that bind returns to properly add and remove from the document – Steven10172 May 15 '17 at 21:39
  • @Steven10172 Good point, since constructor isn't really defined in the React.createClass method, you can always bind in getInitialState(). – Chris Sullivan May 19 '17 at 03:50
  • In relation to the comments above, this is a nice example of where to bind and use the event listeners https://stackoverflow.com/questions/32553158/detect-click-outside-react-component – Craig Myles May 01 '18 at 07:52
  • 1
    Note that `componentWillMount` has been deprecated as of React 16.3. IMO you should instead register the event listeners in `componentDidMount`. – Igor Akkerman Jan 11 '19 at 18:37
  • IMPORTANT: Make sure that both of these are done at the highest level: 1) handleKeyDown function definition and 2) listener setting. It wasn't working for me because I did those two things in one of my child components. When I moved it to the main component and it worked. – Marlo Apr 08 '21 at 05:13
53

If you can use React Hooks, a good approach is to useEffect, so the event listener will be subscribed only once and properly unsubscribed when the component is unmounted.

The example below was extracted from https://usehooks.com/useEventListener/:

// Hook
function useEventListener(eventName, handler, element = window){
  // Create a ref that stores handler
  const savedHandler = useRef();

  // Update ref.current value if handler changes.
  // This allows our effect below to always get latest handler ...
  // ... without us needing to pass it in effect deps array ...
  // ... and potentially cause effect to re-run every render.
  useEffect(() => {
    savedHandler.current = handler;
  }, [handler]);

  useEffect(
    () => {
      // Make sure element supports addEventListener
      // On 
      const isSupported = element && element.addEventListener;
      if (!isSupported) return;

      // Create event listener that calls handler function stored in ref
      const eventListener = event => savedHandler.current(event);

      // Add event listener
      element.addEventListener(eventName, eventListener);

      // Remove event listener on cleanup
      return () => {
        element.removeEventListener(eventName, eventListener);
      };
    },
    [eventName, element] // Re-run if eventName or element changes
  );
};

You also could install it from npm, for example, npm i @use-it/event-listener - see the project here - https://github.com/donavon/use-event-listener.

Then, to use it in your component you just have to call it inside your functional component passing the event name and the handler. For example, if you want to console.log every time the Escape key is pressed:

import useEventListener from '@use-it/event-listener'

const ESCAPE_KEYS = ['27', 'Escape'];

const App = () => {
  function handler({ key }) {
    if (ESCAPE_KEYS.includes(String(key))) {
      console.log('Escape key pressed!');
    }
  }

  useEventListener('keydown', handler);

  return <span>hello world</span>;
}
Brian Burns
  • 20,575
  • 8
  • 83
  • 77
Mate Paiva
  • 640
  • 5
  • 7
  • 1
    if App is not a functional component, cannot use it – ashubuntu Feb 10 '20 at 07:06
  • 3
    Thanks for posting this, helped me fix a massive memory leak in my global keyboard handlers. FWIW, the "save listeners to a `ref`" effect is really key — don't pass your event handlers in the `useEffect` dependency array that adds them to `document.body.onKeyDown`! – aendra Mar 02 '20 at 15:29
  • 1
    @aendrew: What the difference from saving handler to a ref and just declare a function ? – thelonglqd Oct 30 '20 at 07:35
  • @thelonglqd I think because otherwise they get added as event handlers multiple times — don't quote me on that though, that was over half a year ago and my memory's foggy!! – aendra Nov 02 '20 at 11:57
  • 1
    Anyone trying to implement this on NextJS, just add a window object validation check inside `useEventListener` method to ensure it runs on client-side. And also remove the param `element`. `if (typeof window === 'undefined') return; const element = window;` – KeshavDulal Jun 29 '21 at 10:33
  • Finally something that works! Great answer, thanks! – Gass Mar 17 '22 at 16:15
4

A version of Jt oso's answer that is more relevant to this question. I think this is much simpler than the other answers that use external libraries or API hooks to bind/unbind the listener.

var KEY_ESCAPE = 27;
...
    function handleKeyDown(event) {
        if (event.keyCode === KEY_ESCAPE) {
            /* do your action here */
        }
    }
...
    <div onKeyDown={handleKeyDown}>
...
Tongfa
  • 2,078
  • 1
  • 16
  • 14
  • 7
    The item must be focused first. If you want to have a global event listener it might not be triggered because initially the body element is focused. – n1ru4l Apr 13 '20 at 10:36
  • 1
    You can actually use `if (event.key === 'Escape')` – Yifan Ai Dec 14 '20 at 14:16
3

I had the same requirements for a div that was tab-able.

The following code for me was inside of a call to items.map((item)=> ...

  <div
    tabindex="0"
    onClick={()=> update(item.id)}
    onKeyDown={()=> update(item.id)}
   >
      {renderItem(item)}
  </div>

This worked for me!

Jt oso
  • 57
  • 1
1

I wanted to have global event listeners and had weird behavior due to using React Portals. The event was still triggered on the document element despite being canceled on a portal modal component within the document.

I moved towards only using event listeners on a root object that wraps the whole component tree. The problem here was that initially the body is focused and not the root element, thus the events would first be fired once you focus an element within the tree.

The solution I went for is adding a tabindex and automatically focusing it with an effect hook.

import React from "react";

export default GlobalEventContainer = ({ children, ...props }) => {
  const rootRef = React.useRef(null);
  useEffect(() => {
    if (document.activeElement === document.body && rootContainer.current) 
      rootContainer.current.focus();
    }
  });

  return <div {...props} tabIndex="0" ref={rootRef}>{children}</div>
};
n1ru4l
  • 488
  • 1
  • 10
  • 29
0

I struggled with a similar issue while trying to code a wordle clone and identifying key presses and have them do what I expected. Here is my solution:

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

const [random, setRandom] = useState(0.0);
const [key, setKey] = useState("");

const registerKeyPress = useCallback((e) => {
  setRandom(Math.random());
  setKey(e.key);
}, []);
useEffect(() => {
  if (key === "x") console.log("do something");
  }, [random]);
useEffect(() => {
  window.addEventListener("keydown", registerKeyPress);
}, [registerKeyPress]);

I am using a random number to trigger the useEffect() function that actually handles the key press because if the pressed key is the same as the last pressed key, nothing will happen. (For example, in my wordle clone, the word gloom or any word with double letters or using backspace to erase more than one letter did not work.)

** this will cause warnings regarding missing values in the dependency array of useEffect. maybe there's a more proper way to organize the hooks (if anyone knows please comment) but the warnings will not cause errors. you can silence them with this comment: // eslint-disable-next-line react-hooks/exhaustive-deps **

bcstryker
  • 456
  • 3
  • 15