react: touchstart preventDefault() does not prevent click event.
Do you want to request a feature or report a bug? Bug
What is the current behavior?
Calling e.preventDefault()
on a synthetic onTouchStart
event fails to prevent the click
event. I also tried e.nativeEvent.preventDefault()
, but this didn’t make any difference.
If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem via https://jsfiddle.net or similar (template: https://jsfiddle.net/84v837e9/).
Here’s a div which is supposed to handle a hover case but not process a click the first time is is tapped via touch (click on desktop is fine). However tapping with touch (on a mobile device or using dev tools touch emulation) will trigger both touchstart
and click
immediately.
const style = {
background: 'red',
width: 100,
height: 100,
// to ensure `touchstart` `preventDefault()` is allowed on mobile
touchAction: 'none'
};
class SomeButton extends React.Component {
constructor (props) {
super(props);
this.state = {
hover: false,
click: false
};
}
render () {
return (
<div
style={style}
onMouseEnter={() => this.setState({ hover: true })}
onClick={() => this.setState({ click: true })}
onTouchStart={e => {
if (!this.state.hover) {
e.preventDefault(); // doesn't work!
this.setState({ hover: true });
}
}}
>
{this.state.hover && 'hover!'}
{this.state.click && 'click!'}
</div>
);
}
}
However if I move the touchstart
listener to componentDidMount
and use the normal DOM API, everything works:
// ...
class SomeButton extends React.Component {
constructor (props) {
// ...
}
componentDidMount () {
this.elem.addEventListener('touchstart', e => {
if (!this.state.hover) {
e.preventDefault(); // WORKS!
this.setState({ hover: true });
}
});
}
render () {
return (
<div
ref={elem => this.elem = elem}
{ /* ... (removed onTouchStart) ... */}
>
{/* ... */}
</div>
);
}
}
What is the expected behavior?
The first time a touchstart
is processed, we only treat it as a hover, and wait to process the click
event until after the next touchstart
. If the pointer is a mouse, both events can be processed at once.
Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React? React 15.5.4. Not sure about previous React versions. Chrome for Android, Chrome for Mac emulating touch, Firefox for Mac emulating touch.
About this issue
- Original URL
- State: closed
- Created 7 years ago
- Reactions: 16
- Comments: 28 (9 by maintainers)
Commits related to this issue
- Handle touchStart so it prevents button clicks See https://github.com/facebook/react/issues/9809#issuecomment-413978405 — committed to ssorallen/bar-gen by deleted user 5 years ago
- swiper 添加方向锁定(目前有BUG 待修复 https://github.com/facebook/react/issues/9809) — committed to zhongmeizhi/uuz by zhongmeizhi 4 years ago
- Prevent click + touch events from both firing Which caused relations to annoyingly go right into edit mode on click https://github.com/facebook/react/issues/9809#issuecomment-507640770 — committed to allantokuda/radraw by allantokuda 4 years ago
I had this same problem and was able to eliminate the superfluous
mousedown
event by callingpreventDefault()
from withinonTouchEnd
like this:Reference: https://developers.google.com/web/updates/2017/01/scrolling-intervention
Got some feedback this wasn’t clear enough.
The problem is that Chrome made a breaking change and
e.preventDefault()
in document-leveltouchstart
listeners doesn’t work anymore. React binds events at document level for better performance. This is whye.preventDefault()
in ReactonTouchStart
doesn’t currently work.You can work around this by attaching a listener at the individual node level with refs. I showed how to do this in the previous comment (https://github.com/facebook/react/issues/9809#issuecomment-413978405).
Longer term, we plan to change React to attach events at the root container level instead of document level anyway. This is what #2043 is about. So when we implement #2043 the problem will go away by itself. Until then, the workaround with a ref and manual
ontouchstart
listener should work fine.I get the following error when trying to invoke
preventDefault
on a touch event (fromonTouchStart
).react-dom.development.js:6202 Unable to preventDefault inside passive event listener invocation.
Therefore, with the latest version of React, this issue’s title (touchstart preventDefault() does not prevent click event.) is still an issue. @gaearon Can this issue be re-opened?
@nhunzaker I think the problem here is that React adds event listeners as passive (the new default) and that passive event listeners do not allow calling
preventDefault()
(also check out #6436).Checking out your example makes this pretty clear by the following line in the console (you have to tap a few times for the message to pop up):
If I update your example and and mark the event listeners as
{ passive: false }
it works like expected: https://jsfiddle.net/dL4obakf/5/ (am I overlooking something?)A simple workaround in React) is to add a native event listener using the
{ passive: false }
option:It’s also possible to feature-detect passive-event listener support:
Any update on this? It’s been nearly eight months and this is still an issue. I was able to work around it with a solution similar to the one suggested by @OZhurbenko, but it would be really nice to see this fixed.
Hmm… actually “normal DOM API” solution didn’t work. It didn’t fix it for Firefox for Android, there’s a bug there. Firefox triggers mousedown-mousemove-mouseup events following touchstart no matter what😕
Had to work around that and add a flag to the touchStart handler:
And a simple return from mouseDown handler:
In this case it doesn’t matter how you add “touchstart” event listener, I did it via standard React way.
Seems like the same issue as https://github.com/facebook/react/issues/11530. Chrome made a breaking change and this broke us.
As a workaround you can attach the event listener directly to the node via a ref. Here’s an example from the linked issue:
If you attach the listener manually then
e.preventDefault()
in it would work.Longer term, https://github.com/facebook/react/issues/2043 will fix it. Since a workaround exists, and this particular issue itself is not actionable for us, I’ll close this in favor of https://github.com/facebook/react/issues/2043 (which would solve this in the longer term).
I’m seeing this in React 18, so it seems like it isn’t fixed by #2043 which I think went out with 17? I’ve made a reproduction of the problem with 18.2: https://4hrliz.csb.app/
In Chrome with touch device emulation I get an error in the console same as @nz-chris:
Still doing preventDefault in touchstart handler does not suppress any following mouse event. Latest React. Handler attached using onTouchStart. (so passive, I presume)
This works, most of the time. But when I have a nested element and implement double handlers on both a parent and a child, the child’s
e.preventDefault()
only prevents the touch event, but the mouse event is still called on the parent.@gaearon is it safe to assume this old issue is fixed in React 17, via the changes to where event listeners are bound?
I am still confused why ReactJS has been broken for over a year, when @philipp-spiess has provided a trivial fix. Chrome does allow
{ passive: false }
handlers on the document, just by default it is true. Is Chrome no longer supported by ReactJS?Sorry… I let this slide. 😦 I wonder if there is an event plugin that is getting in the way. I’ll investigate what event plugins fire on touch.
@mseddon My fix is not practical to be applied at the framework level. There are a number of arguments why Chrome made some events passive by default and why we don’t want to revert that decision. You can read up on that here: https://github.com/facebook/react/issues/6436
The recommended approach is to only add non-passive events if you’re certain that you will need to call
event.preventDefault()
and you can do this within React as well by manually adding event listeners (as Dan and I pointed out in the comments above).That said, we would really like a declarative way to add passive/non-passive event listeners within the framework but changing that would break numerous React applications in subtle ways which is why we need to be especially careful here. We’re working on a large scale revamp of React DOM called React Fire which should give us what’s needed to address this issue though.
What fun!
It looks like
event.defaultPrevented
is only set on the SyntheticEvent, not the native event:So for some reason, this synthetic event.preventDefault event implementation isn’t working:
https://github.com/facebook/react/blob/master/packages/events/SyntheticEvent.js#L115-L128
Now to figure out why…
It would be great if this was fixed at some point. I was very confused about this behavior, especially after I saw Chromium response saying that everything works as expected, but it clearly didn’t.
Thanks, that worked for me.
I have a custom build up for that branch, you could pull in React and ReactDOM from:
React: https://nh-dom-fixtures-scroll.surge.sh/react.development.js
ReactDOM: https://nh-dom-fixtures-scroll.surge.sh/react-dom.development.js
I’ve forked and updated your gist with those scripts here: http://jsfiddle.net/69z2wepo/79803/
Though I won’t be able to dig into testing it myself until later today or tomorrow.
@EnoahNetzach I became curious after your comment so I tested again…
What I’m finding that is peculiar now is that in Chrome for Mac (58.0.3029.110) emulating touch, the second example solves the issue… but in Firefox for Mac (53.0.3) emulating touch, neither example works as desired!
For the record, the Touch Events spec is clear about what should happen.
Just for completeness, it works as expected on Firefox 53.0.2 on Android 6.0.1, while on chromium based browsers I still get the issue.