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 · 57 comments

Comments

Projects
None yet
@sophiebits
Copy link
Contributor

sophiebits commented Aug 21, 2013

#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

This comment has been minimized.

Copy link
Contributor

petehunt commented Aug 21, 2013

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

This comment has been minimized.

Copy link
Contributor

andreypopp commented Aug 22, 2013

+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

This comment has been minimized.

Copy link
Member

sebmarkbage commented Sep 6, 2013

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

This comment has been minimized.

Copy link
Contributor

sophiebits commented Sep 6, 2013

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

@sophiebits

This comment has been minimized.

Copy link
Contributor

sophiebits commented Sep 6, 2013

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

@Aetet

This comment has been minimized.

Copy link

Aetet commented Feb 15, 2014

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

@syranide

This comment has been minimized.

Copy link
Contributor

syranide commented Feb 18, 2014

@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

This comment has been minimized.

Copy link
Contributor

nick-thompson commented Aug 19, 2014

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

This comment has been minimized.

Copy link
Contributor

glenjamin commented Sep 3, 2014

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

This comment has been minimized.

Copy link

ThomasDeutsch commented Sep 5, 2014

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

This comment has been minimized.

Copy link
Contributor

gasi commented Sep 23, 2014

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

@jsdir

This comment has been minimized.

Copy link

jsdir commented Sep 23, 2014

👍

4 similar comments
@vimto

This comment has been minimized.

Copy link

vimto commented Sep 25, 2014

👍

@pxwise

This comment has been minimized.

Copy link

pxwise commented Nov 14, 2014

👍

@danyx23

This comment has been minimized.

Copy link

danyx23 commented Nov 29, 2014

👍

@byelims

This comment has been minimized.

Copy link

byelims commented Dec 2, 2014

👍

@bloodyowl

This comment has been minimized.

Copy link
Contributor

bloodyowl commented Dec 2, 2014

👍, 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

This comment has been minimized.

Copy link
Member

acdlite commented Dec 2, 2014

👍

@aldendaniels

This comment has been minimized.

Copy link

aldendaniels commented Feb 4, 2015

👍

1 similar comment
@mathieumg

This comment has been minimized.

Copy link
Contributor

mathieumg commented Feb 4, 2015

👍

@jareware

This comment has been minimized.

Copy link

jareware commented Feb 11, 2015

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

This comment has been minimized.

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

This comment has been minimized.

Copy link
Contributor

chicoxyzzy commented Apr 6, 2015

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

@brigand

This comment has been minimized.

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

This comment has been minimized.

Copy link

limelights commented Apr 23, 2015

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

@probablyup

This comment has been minimized.

Copy link
Contributor

probablyup commented Apr 24, 2015

+1 to @brigand's suggestion

@jwietelmann

This comment has been minimized.

Copy link

jwietelmann commented Feb 23, 2016

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?

@kevinbarabash

This comment has been minimized.

Copy link

kevinbarabash commented Feb 29, 2016

+1

@ayrton

This comment has been minimized.

Copy link

ayrton commented Mar 13, 2016

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.

@elado

This comment has been minimized.

Copy link

elado commented May 11, 2016

Referencing my comment #284 (comment)

@elado

This comment has been minimized.

Copy link

elado commented May 11, 2016

Ad-hoc addEventListener on an element is dangrous. If a nested component has React event (onClick) with e.stopPropagation, the event in addEventListener won't be stopped.

@vitalif

This comment has been minimized.

Copy link

vitalif commented Jun 21, 2016

I've discovered that if I add global event handler with document.body.addEventListener('click', ...) in a component I cannot stop it with ev.stopPropagation() inside React event handler (because global native event handler runs BEFORE the react one). But if I set it with window.addEventListener... it stops successfully.
I think it's a bad behaviour.
Is React setting all synthentic events on window instead of specific DOM elements?

@bloodyowl

This comment has been minimized.

Copy link
Contributor

bloodyowl commented Jun 21, 2016

Here's the solution I came to, which works when you have your full page powered by React:

  • Use a provider component at the top of the render-tree passing a subscription mechanism in childContext
  • Style it in order to make it the same size as the document at all times (minHeight: "100vh")
  • Use a component that consumes the context and subscribes to the events

The solution is declarative (you just use <ReactHigherEvent onClick={handleGlobalClick} />). This is open-sourced here : https://github.com/bloodyowl/react-higher-event

@dantman

This comment has been minimized.

Copy link
Contributor

dantman commented Jun 21, 2016

@bloodyowl That won't work for the global events on window like resize.

@bloodyowl

This comment has been minimized.

Copy link
Contributor

bloodyowl commented Jun 21, 2016

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

This comment has been minimized.

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

@probablyup

This comment has been minimized.

Copy link
Contributor

probablyup commented Aug 3, 2016

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

This comment has been minimized.

Copy link
Contributor

sophiebits commented Aug 18, 2016

No near-term plans for this, sorry.

@philipp-spiess

This comment has been minimized.

Copy link
Collaborator

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

This comment has been minimized.

Copy link

samfrances commented Oct 2, 2017

+1

@jamiewinder

This comment has been minimized.

Copy link

jamiewinder commented Oct 4, 2017

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

Block: Cancel deselect via synthetic events
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

Block: Cancel deselect via synthetic events
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

This comment has been minimized.

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

This comment has been minimized.

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.

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