Ian Obermiller

Part time hacker, full time dad.

Keyboard Shortcuts with React

Posted

In many web apps, you want to trigger an action in response to a keypress when a certain component is showing. I ran into this while playing with NuclearMail. Think gmail style "y to archive" kind of shortcuts. So, building with React, we might have a MessageView component, which, when it is showing, we want to respond to the "y" keypress and archive the message. All we need is the keyboardjs library and some decorator glue code:

// KeyBinder.js

var keyboardjs = require('keyboardjs');

module.exports = function decorateWithKeyBinding(
  ComposedComponent,
) {
  var displayName =
    ComposedComponent.displayName ||
    ComposedComponent.name ||
    'Component';

  class KeybinderEnhancer extends ComposedComponent {
    bindKey(keyCombo: string, fn: () => void) {
      this._keyBindings = this._keyBindings || [];
      this._keyBindings.push(
        keyboardjs.on(keyCombo, e => {
          if (
            ['INPUT', 'TEXTAREA'].indexOf(
              e.target.tagName,
            ) >= 0
          ) {
            return;
          }
          fn();
        }),
      );
    }

    componentWillUnmount() {
      if (super.componentWillUnmount) {
        super.componentWillUnmount();
      }

      if (this._keyBindings) {
        this._keyBindings.forEach(binding =>
          binding.clear(),
        );
      }
    }
  }

  KeybinderEnhancer.displayName = `Keybinder(${displayName})`;

  return KeybinderEnhancer;
};

Notice how we ignore bindings fired when an input or textarea has focus is the target, so we don't trigger "archive" while you are searching for something with the letter "y".

Then, decorate your component with @KeyBinder and call this.bindKey in componentWillMount:

@KeyBinder
class MessageView extends Component {
  componentWillMount() {
    this.bindKey('y', this._archive);
  }

  _archive = () => { ... };
}

Notice that _archive uses class properties syntax to avoid using bind. You could also make this into a mixin for easier use with createClass style components, or just use the function call syntax instead, MessageView = KeyBinder(MessageView).

Let me know in the comments if you think this would be useful to publish to NPM!