question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Declarative API for installing global DOM event handlers

See original GitHub issue

#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.)

Issue Analytics

  • State:open
  • Created 10 years ago
  • Reactions:47
  • Comments:59 (25 by maintainers)

github_iconTop GitHub Comments

15reactions
philipp-spiesscommented, May 5, 2017

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. 😊

9reactions
yannvanhalewyncommented, 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

Read more comments on GitHub >

github_iconTop Results From Across the Web

Declarative APIs in an Imperative World - InfoQ
We have this command React. Component which wraps the command registry API and allows you to add new commands.
Read more >
Events - Ractive.js
With Ractive.js, events are declarative instead, and you declare an event handler like this: <button on-click="@global.alert( 'Activating!' )">Activate!
Read more >
chrome.events - Chrome Developers
Example APIs using Events: alarms, i18n, identity, runtime. Most chrome APIs do. # Declarative Event Handlers. The declarative event handlers provide a means...
Read more >
Events - Polymer Project
Annotated event listener setup ... To add event listeners to local DOM children, use on- event annotations in your template. This often eliminates...
Read more >
JavaScript, Events, DOM APIs - A-Frame
Components encapsulate all of our code to be reusable, declarative, and shareable. Though if we're just poking around at runtime, we can use...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found