Skip to content

Declarative API for installing global DOM event handlers #285

Open
spicyj opened this Issue · 43 comments
@spicyj
Facebook member

#284 reminded me that one thing I've sometimes wanted is to install a handler on window for keypress (for keyboard shortcuts) or scroll. Right now I can just do window.addEventListener in componentDidMount but since React is listening already, it would be nice if there were some way for me to intercept those events. (In addition, receiving normalized synthetic events is generally more useful.)

@petehunt

Yes, I've wanted this for resize as well. We talked once about adding maybe a onWindowResize event that fires on every component, but how would it bubble?

@andreypopp

+1 on that, just encountered a case for that

We talked once about adding maybe a onWindowResize event that fires on every component, but how would it bubble?

If it fires on every component does it makes sense for it to bubble?

@sebmarkbage
Facebook member

I wanted this for mousemove and mouseup as well. :) We're thinking about a larger declarative event system that could do conflict resolution but we should probably get this in the mean time. Not sure about the API though.

@spicyj
Facebook member

For mousemove and mouseup, I think @jordwalke was suggesting using ResponderEventPlugin…

@spicyj
Facebook member

I'll also add that a way to bind to onbeforeunload declaratively could be helpful.

@Aetet

Also will be cool to have context keyDown. Like context hokeys for keyboard driven apps.

@syranide

@Aetet Sadly though, all of them operate on the assumption of US/Western keyboard layouts, unless you're willing to avoid support for ctrl and alt. Also, you could easily make this as a Mixin yourself.

@spicyj @petehunt As for this specific PR, what about simply exposing it as React.addEventListener(node, event, callback) (could be useful for when leaving the confines of React, like innerHTML) or as a mixin ReactEventsMixin + listenToEvents: [{event: callback}] which could then take care of the cleaning up itself.

Depending on the use-cases, I guess you could even do ReactWindowResizeMixin + handleWindowResize:, although you might end up with a lot of mixins. Again, depending on the size of the problem, you could even just have a single mixin that attaches to the events that have defined handlers/methods, handleWindowResize, etc.

The mixins could even be implemented as an addon, although it kind of feels like a "native" implementation would be nice.

@nick-thompson

Maybe it would be useful to consider a Flux Store-like solution here? Like some kind of ReactEvents store which wraps window-level events and emits synthetic events. Your components could subscribe and unsubscribe as they see fit.

onWindowResize: function(event) {
  // do whatever you want in response to the resize event
},
componentDidMount: function() {
  this.subscription = ReactEvents.subscribe(ReactEvents.constants.WINDOW_RESIZE, this.onWindowResize);
},

componentWillUnmount: function() {
  this.subscription.remove();
}
@glenjamin

A couple of additional data points on this - the demo in http://kentwilliam.com/articles/rich-drag-and-drop-in-react-js mentions having to drop out of react events to do document listeners for mouse movements.

I've got a small module I put together for handing hotkeys that reaches into a bunch of React internals in order to produce synthetic keyboard events for document key events: https://github.com/glenjamin/react-hotkey/blob/master/index.js - providing a neat top-level listener that can be subscribed to, but forcing components to manage their own subscriptions' lifecycle seems like a reasonable tradeoff to me.

@ThomasDeutsch

An API to hook events into the component events, like @glenjamin described, to produce synthetic events would be a nice thing to have.

this.events.fromEvent( ... )   // events on this components DOM representatoin
this.events.fromEventTarget( ... ) // events from other targets like "document"

I would recommend a look at the Bacon.js wrappers. Maybe a fromCallback binder would be great, too?

It needs to be useable declaratively ( like other events )

// inside the component:
render: function() {
    return (
      <div onMyEvent={handler} > test </div>
    );
}

// from outside of the component, too? ( i do not think so )
<MyComponent onMyCusomEvent={eventhandler} />

and when could it be registered?

// before the first rendering, because of the custom event attribute
componendWillMount: function(events) {
    events.fromEvent( ... ).as('onMyEvent')
}

i think that is basically what @nick-thompson and @syranide were saying.

@gasi

:+1: For React support window-level events such as keydown, keyup, etc. for keyboard shortcuts.

@byelims

👍

@bloodyowl

:+1:, I'd like something like this :

var Modal = React.createClass({
  componentDidMount() {
    React.addEventListener(document, "keyup", this.handleShortcuts)
  },
  componentWillUnmount() {
    React.removeEventListener(document, "keyup", this.handleShortcuts)
  },
  handleShortcuts(eventObject) {
    switch(eventObject.which) {
      case 27:
        this.props.hide()
        break
      // …
    }
  }
  // …
})
@jareware

I'm looking for a solution like this as well! As in, having a standard DOM event called, say, MyWeirdEvent and being somehow able to tell React to start managing it exactly as it does events like click, with e.g.

<SomeComponent onMyWeirdEvent={handler} />

Currently the React event system feels quite exclusive of any 3rd party libs.

@nelix

I think #285 (comment) is a pretty good idea, but its kind of messy.
I normally connect window/document level events to flux or pass it down the app tree.
It would be nice if you could pass an option to React.render to define it as the app entry point, and delegate those events to it.

class App extends React.Document {
  handleResize() { this.forceUpdate() }
  render() { return <div onDocumentResize={this.handleResize.bind(this)}/>; }
}
React.render(<App/>, document.body, {delegateEvents: true});

Kind of off topic, but this could be related to work making react handle being mounted on document.body function more sensibly... If it were safer to mount react on the document body you could delegate body events by default.

@chicoxyzzy

@nelix do events only propagate but not bubble in your proposal?

@brigand

+1 to React.{add,remove}EventListener. Provide the minimum api to hook into react's event system, and let third party libs build on this as they see fit.

@limelights

Couldn't agree more with all above poster, +1

@yaycmyk

+1 to @brigand's suggestion

@robink

+1

@meghprkh

+1 (Currently using mousetrap)

@ctrlaltdylan

:+1: But I'm really digging keymaster

    componentDidMount: function() { 
      key('esc', this.onClose) 
    },                         

    componentWillUnmount: function() {
      key.unbind('esc', this.onClose) 
    },

    onClose: function() {      
        console.log('awoiejf');
    }
@blainekasten

It'd be super rad to use ES7 decorators in a declaritive way like so:

class Component {

  @globalEvent('click')
  onGlobalClick(e) {
    // handle window click
  }

  render() {
    // render
  }
}
@blainekasten

Decorator Comment Edit:

This wouldn't actually be a logical approach to using decorators as they would just be mounted as functions on the class prototype. Not executed automatically when mounted. The only way this could be implemented (without giving it much thought) would be for the @globalEvent decorator to register the method name with a predictable signature that could be searchable.

Something like:

@globalEvent('click')
onGlobalClick(e) { ... }

// compiled
_reactGlobalEvent{UUID}: handler

Then there would have to be some sort of mechanism internally that searches for similar keys:

/*
 * @internal
 */
function bindGlobalHandlers(component) {
  for (var method in component ) {
    if (method.indexOf('_reactGlobalEvent') === 0) {
       React.bindGlobalMethod(method);
    }
  }
}

Here is a potential implementation approach to the decorator:

function globalEvent(eventType) {
  return function decorate(target, key, descriptor) {
    function reactGlobalBind() {
      React.globalEvent(eventType, descriptor.value)
    }

    Object.defineProperty(
      target,
      '_reactGlobalEvent' + Date.now(), // not a good UUID
      { value: reactGlobalBind }
    );
  }
}

class Component extends React.Component {
  @globalEvent('click')
  onGlobalClick(e) {
    console.log('clicked');
  }
}
Full discretion

This is basically an abuse of the decorator pattern
But it does make a nice implementation detail. Though it doesn't appeal to those who can't utilize the decorator pattern.

@blainekasten

fwiw, I created an abuse of this pattern as an example: https://github.com/blainekasten/react-global-event-decorator/tree/master

@brigand

@blainekasten different take on the same thing: https://github.com/brigand/react-global-event-method

I don't really like either of them though, specifically for the componentDidMount/etc issue. Maybe annotating the functions with the method decorators and still using a class decorator would be the way to go?

It'd still be cool to have this in core, mostly so we get the same event shimming benefits normal react events give.

@blainekasten

The componentDidMount issue could be solved if this was an internal react implementation. But again, I'm not selling this as the way to go. Just a thought and experiment.

@blainekasten blainekasten referenced this issue in blainekasten/shortcut.js
Open

Investigate best way to interact with React. #6

@narqo

:+1: here.

API like onDocument<EventName> and onWindow<EventName> would be very handy. As well as imperative React.addEventListener(element, <EventName>, ...).

There are millions of articles currently which suggest to use simple <node>.addEventListener() in the componentDidMount hook in case one needs it, but no one clarifies that such things should be done through event delegation. At least helpers like React.addEventListener could prevent redundant memory consumption in most of react plugins out the box.

@milesj

Why not make use of the current JSX on event handlers and extend them? Anything that should be bound to the window, or the document, or global, should simply be prefixed, like so?

class Foo extends React.Component {
    render() {
        return (
            <input
                onKeyDown={this.onKeyDown} // Element event
                onWindowKeyDown={this.onWindowKeyDown} // Window event
                onDocumentSelectionChange={this.onDocumentSelectionChange} // Document event
                onGlobalMouseWheel={this.onGlobalMouseWheel} // Global event 
                />
        );
    }
}

This approach does not require lifecycle events, decorators, new syntax, new external APIs, is easy to use and understand, is clean and straightforward, etc. It's akin to appending Capture to existing events.

Personally, it would be a better idea to improve React's internal event system, by providing new event types, instead of moving this logic to the component layer.

@JKillian

:+1: (decorators don't feel like the right pattern for this to me though)

@jwietelmann

On the topic of patterns for this, just how strongly-discouraged is context these days? I just took a cue from redux and wrote some code to handle CSRF tokens with an AuthenticityTokenProvider component that passes down the data via context and then a child AuthenticityToken that receives it. Would a GlobalKeyEventsProvider, GlobalMouseEventsProvider, etc. be a solid direction?

@ayrton

Having stumbled upon more or less the same issues outlined in this issue, I've created a little npm package called react-key-handler. If this is something react team is willing to integrate directly into react, I'd be more than happy to help out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.