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

Support Passive Event Listeners #6436

Open
sebmarkbage opened this issue Apr 7, 2016 · 59 comments
Open

Support Passive Event Listeners #6436

sebmarkbage opened this issue Apr 7, 2016 · 59 comments

Comments

@sebmarkbage
Copy link
Member

@sebmarkbage sebmarkbage commented Apr 7, 2016

https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md

It would be good to have everything be passive by default and only opt-in to active when needed. E.g. you could listen to text input events but only preventDefault or used controlled behavior when you have active listeners.

Similarly, we could unify this with React Native's threading model. E.g. one thing we could do there is synchronously block the UI thread when there are active listeners such as handling keystrokes.

cc @vjeux @ide

@jhgg
Copy link

@jhgg jhgg commented May 29, 2016

This landed in Chrome 51. Is there any updated plan to support this in React? :O

@aleksandar-b
Copy link

@aleksandar-b aleksandar-b commented Jul 29, 2016

How is this possible if React has only one event listener on document, and then delegates to others?
@sebmarkbage

@followdarko
Copy link

@followdarko followdarko commented Aug 17, 2016

What's the current status of issue with Passive Events ?

@radubrehar
Copy link

@radubrehar radubrehar commented Aug 18, 2016

I just hit a warning in chrome about handling the wheel event, which could be optimized if it were registered as a passive event handler. So having this in React would be neat!

@nolanlawson
Copy link
Contributor

@nolanlawson nolanlawson commented Sep 29, 2016

You'll also want to handle arbitrary options, such as once which has already landed in Firefox nightly: https://twitter.com/mozhacks/status/758763803991474176. Full list: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener

@sebmarkbage
Copy link
Member Author

@sebmarkbage sebmarkbage commented Sep 29, 2016

FWIW, Facebook listens to active wheel events to block outer scrolling when sidebars or chat windows are scrolled. We can't implement the UI without it. We still want to support this as an option but the problem space is still incomplete so there might evolve alternative solutions to this problem that doesn't involve passive event listeners. So it is still an active design space.

@romulof
Copy link

@romulof romulof commented Sep 29, 2016

It's important to keep both active listeners and add support passive ones.
On desktop applications you don't see any difference, but on mobile apps passive scroll listeners give a great speed boost.

Little suggestion:

<SomeElement
  onScroll={this.onScrollThatCallsPreventDefault}
  onScrollPassive={this.onScrollThatJustListens}
  ...this.props
/>
@radubrehar
Copy link

@radubrehar radubrehar commented Sep 30, 2016

@romulof yeah, this is how you register events on the capture phase as well

<SomeElement
  onClick={this.onClick}
  onClickCapture={this.onClickCapture}
  onScrollPassive={this.onScrollPassive}
/>

so I imagine this would be the proper API to support passive events as well.

Side note: a tricky question is - how would you register passive events for the capture phase? I suppose this is not possible, by the nature of passive events. Since they are even not allowed to call event.preventDefault(), so probably this is a non-issue.

@romulof
Copy link

@romulof romulof commented Sep 30, 2016

@radubrehar, onScrollCapturePassive looks like the whole bible in camel-case.

@radubrehar
Copy link

@radubrehar radubrehar commented Sep 30, 2016

:) It's not the case, since there are no passive events on the capture phase.

@romulof
Copy link

@romulof romulof commented Sep 30, 2016

Sure it doesn't make sense, but I would't count on it. There's also other types event binding, such as once.

Another suggestion:

<SomeElement
  onScroll={this.onScrollThatCallsPreventDefault}
/>
<SomePassiveElement
  onScroll={{
    passive: true,
    capture: true,
    handler: this.onScrollThatJustListens,
  }}
/>

This way React would have to detect whether the event handler is a function (normal binding), or and object containing binding options and the handler function.

@lencioni
Copy link
Contributor

@lencioni lencioni commented Oct 12, 2016

I think the object approach with options makes more sense than onFooPassive, since there are other options that might be needed. If combined with @sebmarkbage's suggestion that events should be passive by default, this probably wouldn't be too cumbersome.

Another approach that comes to mind would be to attach properties to the event handler to allow them to opt out of passive mode (or toggle other options). Something like this:

class Foo extends React.Component {
  constructor() {
    this.handleScroll = this.handleScroll.bind(this);
    this.handleScroll.passive = false;
  }

  handleScroll() {
    ...
  }

  render() {
    return <div onScroll={this.handleScroll} />;
  }
}

In theory, this would work pretty nicely with decorators, once they land.

@lencioni
Copy link
Contributor

@lencioni lencioni commented Oct 17, 2016

Thinking about this a little more, I think it would be better to add an event options property to the function, instead of individual options. That would allow React to only have to worry about one property instead of potentially many. So, to adjust my example above:

class Foo extends React.Component {
  constructor() {
    this.handleScroll = this.handleScroll.bind(this);
    this.handleScroll.options = { passive: false };
  }

  handleScroll() {
    ...
  }

  render() {
    return <div onScroll={this.handleScroll} />;
  }
}

Another thought that occurred to me is what might this look like if we modified JSX syntax in a way that allowed for these options to be passed in via the JSX. Here's a random example that I haven't put much thought into:

return <div onScroll={this.handleScroll, { passive: false }} />;

I've also been thinking about whether events should be passive by default or not, and I'm a bit on the fence. On one hand, this would certainly be nice for events like scroll handlers, but I worry that it would cause too much turbulence and unexpected behavior for many click handlers. We could make it so some events are passive by default and others are not, but that would probably just end up being confusing for folks, so probably not a good idea.

@romulof
Copy link

@romulof romulof commented Oct 17, 2016

This way is pretty similar to what I proposed earlier, without modifying JSX syntax.

return <div onScroll={{ handler: this.handleScroll, passive: true }} />;

And documentation would be straightforward:

div.propTypes = {
  ...
  onScroll: React.PropTypes.oneOf([
    React.PropTypes.func,
    React.PropTypes.shape({
      handler: React.PropTypes.func.isRequired,
      capture: React.PropTypes.bool,
      passive: React.PropTypes.bool,
      once: React.PropTypes.bool,
    }),
};
@joshjg
Copy link

@joshjg joshjg commented Oct 22, 2016

Are react events passive by default? It seems to be that way for touch events, at least. I am not able to preventDefault unless I fall back to vanilla document-level event listeners.

@benwiley4000
Copy link

@benwiley4000 benwiley4000 commented Oct 27, 2016

@joshjg React handlers are passed "synthetic events," which are sort of like native events, but different. By the way, someone with more knowledge should correct what I'm about to say because I haven't actually read the code that does this.

I'm not super familiar with the implementation details, but I know that preventDefault works at least as long as the handlers you're preventing are also React event handlers. That's been my experience, anyway.

With stopPropagation you're more likely to be out of luck (e.g. you have a document click listener which can't be bound with React, and you want to avoid bubbling up if you click inside a certain element). In that case you can use:

function stopPropagation (e) {
  e.stopPropagation();
  e.nativeEvent.stopImmediatePropagation();
}

[MDN]

This got slightly off the main topic, but the short answer is that React doesn't use passive events, they're just sometimes handled in a strange order.

@radubrehar
Copy link

@radubrehar radubrehar commented Mar 20, 2017

@joshjg @benwiley4000 @gaearon Recently the chrome team has changed their approach to document-level touch events, making them passive by default. And since React attaches events at document-level, you get this new behaviour.

See https://www.chromestatus.com/features/5093566007214080

This has indirectly changed they way React behaves - I suppose React does not explicitly mention passive: false when attaching events - hence the change in behavior.

I just hit this as well - so you need to register touch events by hand, with addEventListener

@kychanbi
Copy link

@kychanbi kychanbi commented Mar 26, 2019

[Intervention] Unable to preventDefault inside passive event listener due to target being treated as passive. See https://www.chromestatus.com/features/6662647093133312
It happens again, when I tried to block the default scrolling behavior in onWheel event listener

@ke1vin1993
Copy link

@ke1vin1993 ke1vin1993 commented Mar 26, 2019

@kychanbi Same to me, but I only meet this error on Windows chrome.

@ke1vin1993
Copy link

@ke1vin1993 ke1vin1993 commented Mar 26, 2019

@kychanbi Oh, it's a Chrome 73's feature that treats document level Wheel/Mousewheel event listeners as Passive

@madcher
Copy link

@madcher madcher commented Mar 27, 2019

You can use css property on your component container div touch-action: none

.container {
touch-action: none;
}

@kychanbi
Copy link

@kychanbi kychanbi commented Mar 28, 2019

@madcher it seems does not work for mouse events.
I finally solved it by using native javascript
element.addEventListener("wheel", eventHandler);

@markpradhan
Copy link

@markpradhan markpradhan commented Mar 29, 2019

A small snippet to help those facing this issue:

import React, { useRef, useEffect } from 'react'

const BlockPageScroll = ({ children }) => {
  const scrollRef = useRef(null)
  useEffect(() => {
    const scrollEl = scrollRef.current
    scrollEl.addEventListener('wheel', stopScroll)
    return () => scrollEl.removeEventListener('wheel', stopScroll)
  }, [])
  const stopScroll = e => e.preventDefault()
  return (
    <div ref={scrollRef}>
      {children}
    </div>
  )
}

const Main = () => (
  <BlockPageScroll>
    <div>Scrolling here will only be targeted to inner elements</div>
  </BlockPageScroll>
)
@Fonger
Copy link

@Fonger Fonger commented Apr 3, 2019

@madcher

Somehow onWheel props doesn't work with css touch-action: none;

    componentRef = React.createRef(null);
    handleWheel = (e) => {
      e.preventDefault();
    }
    render() {
      <Container style={{ touchAction: 'none' }} onWheel={this.handleWheel}>
        ...
      </Container>
    }

still getting this error:
[Intervention] Unable to preventDefault inside passive event listener due to target being treated as passive. See <URL>

Working Version

alternative to @markpradhan 's hooks solution, if you're still using old-style component, you can do this:

    componentRef = React.createRef();
    handleWheel = (e) => {
      e.preventDefault();
    }
    componentDidMount() {
      if (this.componentRef.current) {
        this.componentRef.current.addEventListener('wheel', this.handleWheel);
      }
    }
    componentWillUnmount() {
      if (this.componentRef.current) {
        this.componentRef.current.removeEventListener('wheel', this.handleWheel);
      }
    }
    render() {
      <Container ref={this.componentRef}>...</Container>
    }
@eps1lon
Copy link
Contributor

@eps1lon eps1lon commented Apr 3, 2019

@Fonger Probably due to #14856

wKich added a commit to skbkontur/retail-ui that referenced this issue Apr 8, 2019
Hotfix workaround for chrome 73 https://www.chromestatus.com/features/6662647093133312. Due react
don't support passive event listener we are subscribe on these events through `addEventListener`
with `{ passive: false }` option. Current status of supporting passive events in react
facebook/react#6436

fix #1326
brianc added a commit to cruise-automation/webviz that referenced this issue Apr 12, 2019
Recently chrome changed the behavior of wheel listeners to be passive by default.  Passive listeners cannot cancel event propagation.  When a wheel event propagates outside of the canvas it can drastically slow down the browser as the parent elements try to compute whether or not they can scroll.  This change uses manual event listener management to add active listeners, which is inline w/ the behavior for chrome before version 73.

Test plan: use mousewheel. Notice there are no console.warn messages about passive listeners anymore.

Semver impact: this is semver patch.  Bugfix only.

Further reading: facebook/react#14856
facebook/react#6436
brianc added a commit to cruise-automation/webviz that referenced this issue Apr 12, 2019
Recently chrome changed the behavior of wheel listeners to be passive by default.  Passive listeners cannot cancel event propagation.  When a wheel event propagates outside of the canvas it can drastically slow down the browser as the parent elements try to compute whether or not they can scroll.  This change uses manual event listener management to add active listeners, which is inline w/ the behavior for chrome before version 73.

Test plan: use mousewheel. Notice there are no console.warn messages about passive listeners anymore.

Semver impact: this is semver patch.  Bugfix only.

Further reading: facebook/react#14856
facebook/react#6436
@nmn
Copy link
Contributor

@nmn nmn commented Feb 27, 2020

Something like this has been suggested but here are a couple more ideas.

function MyComponent() {
  function onScroll(event) { /* ... */ }
  onScroll.options = {capture, passive, ...};
  return <div onScroll={onScroll} />;
}

This one would let you easily opt into passive events or capture events without the need for a breaking change. However, I was intrigued by the idea of a passive by default event listener. I remember preventDefault being a major obstacle (among others) that blocks React from running in a worker.

function MyComponent() {
 function onScroll(event) { /* ... */ }
 onScroll.shouldPreventDefault = (event): boolean => {
   // some logic to decide if preventDefault() should be called.
 }
 onScroll.shouldStopPropagation = (event): boolean => {
   // some logic to decide if stopPropagation() should be called.
 }
 return <div onScroll={onScroll} />;
}

It would be hard to ensure this doesn't become a breaking change, but if this was enforced, all the code to decide if an event needed to be preventDefaulted would be isolated in code and React would be able to run just that part on the main thread and run everything else in a separate worker or asynchronously.

@dbismut
Copy link

@dbismut dbismut commented Jun 4, 2020

Until this is resolved I think it would be best if references to event.preventDefault() are removed from the docs or are at least marked with a warning regarding Chrome's inability to preventDefault on passive events.

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

Successfully merging a pull request may close this issue.

None yet
You can’t perform that action at this time.