Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Declarative API for installing global DOM event handlers #285

Open
sophiebits opened this issue Aug 21, 2013 · 60 comments
Open

Declarative API for installing global DOM event handlers #285

sophiebits opened this issue Aug 21, 2013 · 60 comments

Comments

@sophiebits
Copy link
Collaborator

#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
Copy link
Contributor

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
Copy link
Contributor

+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
Copy link
Collaborator

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.

@sophiebits
Copy link
Collaborator Author

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

@sophiebits
Copy link
Collaborator Author

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

@Aetet
Copy link

Aetet commented Feb 15, 2014

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

@syranide
Copy link
Contributor

@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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link

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
Copy link
Contributor

gasi commented Sep 23, 2014

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

@jsdir
Copy link

jsdir commented Sep 23, 2014

👍

4 similar comments
@vimto
Copy link

vimto commented Sep 25, 2014

👍

@pxwise
Copy link

pxwise commented Nov 14, 2014

👍

@danyx23
Copy link

danyx23 commented Nov 29, 2014

👍

@byelims
Copy link

byelims commented Dec 2, 2014

👍

@bloodyowl
Copy link
Contributor

👍, 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
      // …
    }
  }
  // …
})

@acdlite
Copy link
Collaborator

acdlite commented Dec 2, 2014

👍

@aldendaniels
Copy link

👍

1 similar comment
@mathieumg
Copy link
Contributor

👍

@jareware
Copy link

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
Copy link

nelix commented Apr 6, 2015

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
Copy link
Contributor

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

@brigand
Copy link
Contributor

brigand commented Apr 6, 2015

+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
Copy link

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

@quantizor
Copy link
Contributor

+1 to @brigand's suggestion

@bloodyowl
Copy link
Contributor

Yes, of course, it can't put events that are not listened by React on the same phase, it solves the issue for use-cases like listening to a click outside & mousemove/up though.

@tribou
Copy link

tribou commented Jul 17, 2016

Is this still being considered? It would be nice if React could somehow encourage users to do small tasks optimally like when changing a header background color based on scroll position.

I encountered this on a project, and my instinct was to use setState until I ran across this SO issue/answer relating to setState performance: http://stackoverflow.com/a/35467176/1510454

This is the component I ended up with which also included support for passive event listening: https://gist.github.com/tribou/d405436286807eeff669ad4d909331f5

@quantizor
Copy link
Contributor

I've been wanting this as well. Lacing native event listeners into component lifecycle events feels very haphazard and opens up the potential for memory leaks & exceptions if steps aren't taken to properly tear down the listeners during unmount.

@sophiebits
Copy link
Collaborator Author

No near-term plans for this, sorry.

@philipp-spiess
Copy link
Contributor

philipp-spiess commented Oct 13, 2016

Right now, the only way to respond to "outside world" events is to leave the React's event system and add a native DOM listener. This is bad, since it will require more mental overhead when you have to work with this (you need to think about your event listener receiving a native event, or a react synthetic event). It will also simply not be possible for computed SyntheticEvents (e.g. onChange).

It also makes it very hard for react events handlers to interrupt the DOM handlers (This issue is mentioned above). Consider the following example, where it's not intuitive why the React listener can not stop propagation to the document. (Spoiler: React also listens on document, that's why you'd have to use SyntheticEvent#nativeEvent.stopImmediatePropagation():

class ExampleComponent extends React.Component {
  render() {
    return (
      <div onKeyDown={(e) => e.stopPropagation()} />
    )
  }
}

document.addEventListener('keydown', () => {
  alert('why does this still fire?')
})

ReactDOM.render(
  <ExampleComponent name="react"/>,
  document.getElementById('react')
)

An example for when you want to deal with outside events is a simple drawing tool, that must listen on keyup to stop the drawing process - Otherwise, the UI would feel broken. Right now, without leaving React's event system, I could only listen on mosueup event at my own root component and pass this callback to the child that's responsible for the drawing but I can't listen on those mouseup events outside my component or even outside the browser (although React's event hub would capture those by listening on document).

There are a lot of solution ideas - most of them are tied to DOM specific features like document or window. I don't think that this is a way that React would like to go - that's why I think we should make the approach more abstract.

I can think of a new public API, something like an EventRoot. It should behave like a regular DOM Node, so that you can addEventListener() and removeEventListener(), but its callbacks will receive the SyntheticEvent. The EventRoot is created for every root react component (where instance._hostParent === null. It should be accessible inside components by calling something like this.eventRoot.addEventListener() so that it's trivial to migrate for people that are currently relying on DOM event systems (e.g. document.addEventListener()). (Edit: This API could be made declarative as well e.g. onRootMouseDownCapture.)

The EventRoot get involved when triggering a two-phase dispatch. It respects the capture and bubble order as well as stopPropagation(). Everything you'd expect when listening on document. But stopping propagation will be isolated to the specific React instance => Two react trees that listen on the EventRoot can't interfere.

This API should help to further abstract the fact that React will listen on document so that people don't need to rely on this fact anymore.

For the above example, you'd only have to replace document with the new event root. The stopPropagation() can now correctly be applied.

I'd love to hear what you think about this and how I could help shape the future of React's event system. 😊

@samfrances
Copy link

+1

@jamiewinder
Copy link

With the advent of portals in React 16, it's the first time for me that React's event system has felt so dramatically different to that of the DOM. As I raised in #10962, the fact that events bubble through portals is very handy and so far seems to make logical sense, but is not something that works nicely with the current fallbacks to adding DOM events.

I think this divergence makes the need for such an API into React's event system even more relevant now.

aduth added a commit to WordPress/gutenberg that referenced this issue Jan 9, 2018
More reliable when using virtual event bubbling (e.g. portals), but as workaround to noted issue of document-level event binding, need to stop propagation.

See: facebook/react#285
aduth added a commit to WordPress/gutenberg that referenced this issue Jan 9, 2018
More reliable when using virtual event bubbling (e.g. portals), but as workaround to noted issue of document-level event binding, need to stop propagation.

See: facebook/react#285
@yannvanhalewyn
Copy link

yannvanhalewyn commented Mar 17, 2018

Actually, none of the solutions mentioned above were sufficient for me, and I thought I had a pretty general case. I needed some simple global hotkeys. Binding them natively on document in component-did-mount worked of course, like other solutions using mousetrap or keymaster. The problem is, like @philipp-spiess illustrated, any other input field receiving synthetic keydowns and on which stopPropagation have been called are still fired up to the native document keydown listener. This is especially annoying when you have hotkeys that aren't prefixed (meta, alt, ctrl) like 'q' or 'v' => anytime a user inputs that key in an input field a global hot key would be called.

For anyone having the same problem, here's a neat little solution/trick I came up with that might help you and has not been offered in this thread or anywhere for that matter: Bind it twice - once on document, and once at the top of your react tree. The document handler checks if e.target == document.body (or whatever fits your needs), if so it fires. All the other ones are caught by the one bound to the react root. This way:

  • Global key events trigger hotkeys
  • Local key events can use stopPropagation to prevent the event from bubbling to the top of the react tree, or not and the hot key fires.

This can of course be applied to any other events, like clicks etc..

A very simple mockup of the idea

function onKeyDown(e) {
  // Handle global keydowns. !Warning: may receive native or synthetic events
}

function onKeyDownNative(e) {
  // Or whatever assertion works for your usecase, whatever is 
  // "outside" of the react tree.
  if (e.target === document.body) { 
    onKeyDown(e);
  }
}

// Wrap this around the entire app
class HotkeyListener extends React.Component {
  componentDidMount() {
    document.addEventListener("keydown", onKeyDownNative);
  }
  
  componentWillUnmount() {
    document.removeEventListener("keydown", onKeyDownNative);
  }
  
  render() {
    // Listens to any propagated synthetic keydown events
    return <div onKeyDown={onKeyDown}>{this.props.children}</div>;
  }
}

ReactDOM.render(
  <HotkeyListener>
    // This input will propagate and trigger global key event through the synthetic event handler
    <input type="text" />
    // This one will not
    <input type="text" onKeyDown={(e) => e.stopPropagation()} />
  </HotkeyListener>
  , document.getElementById("app"))

Working demo on Codepen

@Zarel
Copy link

Zarel commented Mar 31, 2018

(Wow, no progress in five years? Doesn't Facebook itself support keyboard shortcuts and dismissing popups by clicking outside them?)

I got bitten by this today, when I refactored something from listening to keypresses on an <input> (in a React event) to using document.addEventListener (in a native event) – I was calling setState a lot, and suddenly not batching them made everything a lot laggier. I had to recover performance using ReactDOM.unstable_batchedUpdates.

@brigand
Copy link
Contributor

brigand commented Mar 26, 2019

In current React, it would make sense to offer this as hooks, which also do update batching on all listeners of the type. This would be a significant value add, as React is serving as a hub for all of our own code and react packages we depend on... to provide efficient updates.

@gaearon
Copy link
Collaborator

gaearon commented Aug 10, 2020

Regarding #285 (comment) in particular, we're switching React 17 to register events at the roots, which solves that particular case.

@jesraygarciano
Copy link

perhaps close it already?

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

No branches or pull requests