react: Chrome 73 breaks wheel events
Similar to #8968, but for the wheel and mousewheel events. They are now passive by default for root elements in Chrome 73 (currently beta) which means React apps that have custom scrolling/zooming behaviors will run into issues.
The quick fix may be to manually add event listeners with {passive: false} but has the React team considered if this should be configurable for the React event handler?
Blog post from the Chrome team here: https://developers.google.com/web/updates/2019/02/scrolling-intervention
About this issue
- Original URL
 - State: closed
 - Created 5 years ago
 - Reactions: 45
 - Comments: 40 (8 by maintainers)
 
Commits related to this issue
- fix onWheel preventDefault() on Chrome >= v73 reference: https://github.com/facebook/react/issues/14856 https://www.chromestatus.com/features/6662647093133312 — committed to Fonger/react-pan-and-zoom-hoc by Fonger 5 years ago
 - fix onWheel preventDefault() on Chrome >= v73 reference: https://github.com/facebook/react/issues/14856 https://www.chromestatus.com/features/6662647093133312 — committed to Fonger/react-pan-and-zoom-hoc by Fonger 5 years ago
 - Make wheel listener active 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... — committed to cruise-automation/webviz by brianc 5 years ago
 - Make wheel listener active (#127) Recently chrome changed the behavior of wheel listeners to be passive by default. Passive listeners cannot cancel event propagation. When a wheel event propagates ... — committed to cruise-automation/webviz by brianc 5 years ago
 - workaround for Chrome's change of default behavior of passive event listener. see: https://github.com/facebook/react/issues/14856 — committed to railroad-editor/railroad-editor by kota65535 5 years ago
 - JSWindow: Use native wheel event listener to preventDefault https://github.com/facebook/react/issues/14856 — committed to prometheon/jswf-react by AmaanC 4 years ago
 - Add +/- shortcuts for zoom toggling Replaces the mouse wheel action in 2.3. There are problems with preventing default on it: see https://github.com/facebook/react/issues/14856 Using a keyboard shor... — committed to klembot/twinejs by deleted user 2 years ago
 
@byronwall I strongly believe the Chrome team thinks very carefully and thoughtfully about the changes they make, and I don’t believe it is right to call into question what they should or should not be doing without the full holistic picture of the drive behind the change and the expected outcome. It might be nice to get direct input here from someone who can provide a larger context that can help drive the path forward in a healthy way.
Chrome shouldn’t be changing default options that have such a significant impact on usability. Every site that relies on default
{passive:false}behavior is now broken. Even if React pushes a fix, this is still broken for existing sites. The pace at which Chrome is willing to break standards is staggering. Thepassiveoption wasn’t in the wild until Chrome 51 (June 2016) and now the default has been changed. Consider me unsympathetic to doing what “Chrome believes is important”.This is a temporary fix I’ve been using in the load file
index.js.Ran into this problem with trying to prevent browser zoom on control + wheel and performing a scaling action in our app instead, since native events didn’t fit our use case I ended up with the following solution.
The use effect will bind a non passive event that prevents browser zoom and cleans itself up on unmount, then leaves your react event listeners to do their thing.
Sure thing, here’s a repro:
https://codesandbox.io/s/6zn44nmjvn
In Chrome stable / Safari / etc the box will move but the rest of the page will remain static. In Chrome 73, the entire page rubber bands as you scroll around (the box also moves). Since rubber banding only happens with a trackpad, make sure to test it with one.
Also note all the red errors in the console due to the intervention.
It’s not fair to blame Chrome at all, it’s an issue with React that binds listeners to the root element, Chrome’s reasoning that most of web applications (if used DOM API correctly) won’t be affected.
It’s important for people who reach this page to know that they have the right to demand the fix from React.
I understand the performance concerns that led React team to decide to bind listeners to root elements, but a better design would probably be this: let the developer decide whether he wants to attach the listener to root element (the default behavior) or to attach it to the original element (
onClickandonElementClickfor example)Let me summarize the issue:
Therefore, there are two solutions. Either React adds some way to control passive-ness of events (tracked in https://github.com/facebook/react/issues/6436). Or you use native event listeners for the range of use cases where you really need this.
Since using native event listeners already works (and has always worked) I don’t think there’s anything actionable left in this issue. Whatever API React could theoretically provide, it won’t give you the same amount of flexibility that using the browser API directly would give you. So it seems like a reasonable solution in the meantime.
Talking with @sophiebits: it sounds like we should hold off on making a change until we clamp down a passive event listener API.
We shouldn’t undermine changes Chrome believes are important for improving site performance. If they have found that most scroll/wheel event listeners should be passive, that extends to React apps as well. We’re shouldn’t contribute to making apps slower for most cases when they don’t need to be.
Until a passive event API is available to React, one way to work around this is to use a ref to attach a non-passive event listener. (Edit: this is a work in progress. See https://github.com/facebook/react/pull/15036).
I’m not super familiar with using TypeScript with hooks, but I’ve done my best to form @blixt’s example to use a ref to attach a passive listener:
https://codesandbox.io/s/zzqxp1yvy3
I imagine others will come to the issue board confused about this change, so it’ll be important to have a clear path forward for future issues. Are there ways to improve on the solution above?
Translated: we’re comfortable breaking 2% of those sites which currently rely on default behavior…
My main issue is with Chrome and not React. Having said that, it’s sensible to “unbreak” React applications relying on previously default behavior while a firm API is proposed for declaring passive events. The push for potential performance gains should be balanced against backward compatibility. In this case Chrome has overstepped. Going against their grain until the full API is in place seems a minor transgression.
What breaking changes are there? Until a week ago this was the default behavior. It is still the default behavior for non-Chrome browsers. Forgive me if I am missing the larger picture here?
@catamphetamine Sorry to hide your comment, but it doesn’t help us come to a resolution and I want to keep this issue focused for others coming to the React issue board that might be confused.
The Chrome team didn’t do this in a vacuum, and took the time to research this thoroughly, as linked in the original issue description (https://developers.google.com/web/updates/2019/02/scrolling-intervention):
Much like React, Chrome is in a position to improve user experience across a broad base of users. Both teams are aligned in this goal, even if coordinating on a change like this could have been smoother.
Forcing wheel events to be impassive would create additional breaking changes for React users, while undermining performance improvements on the platform. Let’s figure out the best API for passive event handlers in React moving forward. However in the interim, let’s also come up with a good general purpose solution so that it’s easier for React users to handle this change.
@gaearon Please reopen, since a proper fixes has not yet been provided. As long as this issue is closed, it might get overlooked or assumed to be solved by contributors.
This happens to me on Firefox now as well.
What a dumb gotcha. And the justification is just as dumb: “We don’t need to fix the framework cuz you can use the escape hatch in the framework to make it work right!” Then why am I bothering with your framework?
Incorrect,
wheelevent is onlypassiveon root elements:https://developers.google.com/web/updates/2019/02/scrolling-intervention
Unrelated. We are using React to create non-root elements, so
wheelevent MUST continue to be non-passive by default to conform Web standards.You are doing the opposite of how web browsers work
Native event listeners work, so React should also work.
Is there a solution yet? It’s really important that React doesn’t break existing browser functionality, and
peventDefaultis one of those. On top of that, I see a LOT of hacking around this issue, and most of that hacking is super ugly. We shouldn’t encourage things like that to be neccesary. We shouldn’t need to work against the framework, but with it.So if there’s a proper/clean solution, please provide one. For the length of time that this issue has been ongoing, frankly I bloody well expect one. How hard can it be? Other event can be preventDefault’ed perfectly fine.
I might be “out of league” and I’m probably not seeing the big picture here, but can’t we just have an API change?
onWheelis not the only event where we want to have more control, butscrollalso as well sometimes.A general support for
addEventListeners’soptionsparam would solve this problem once and for all.My idea would be to add support to accept arrays for event props.
This would give us fine-grained control over any event options, and it can keep the default behavior.
Thank you, I’m aware of that.
At the time the change was introduced, React was subscribing on the
document. So at the time Chrome made that change, it did change the behavior for React applications globally. The impact of the change was globally improved scrolling performance on the web. Even though React has switched to using event listeners at the mount point (which is, for most React apps, lower but not much lower than thedocument), we’ve kept thepassivebehavior in React 17 to honor the intent (and impact) of Chrome’s change. Since otherwise we’d effectively undo much of the performance improvement. The thinking is that since React 17 came a year after the change was made, apps using React have already implemented workarounds.I understand your point of view but I hope you see where I’m going with this as well. If not, I’m not sure I can do much to convince you. But the workaround using native event listeners still works.
So we changed from “make it correct, make it fast, make it pretty”, to “make it fast, make it correct, make it pretty” now?
Urgh… That’s not the direction things should go in. Correctness is more important than performance, because you can make a default correct system fast by optimizing it, but it’s ridiculous to make a default incorrect system fast, because the incorrect thing doesn’t actually solve your problem.
Chrome team has numbers to support them, and they didn’t change non-root elements. Do you have any data shows that most React users that are usingwheelevents don’t want to callpreventDefaultin it? At least everyone in this thread (and everyone +1) were surprised when it doesn’t work like native DOM.EDIT: This part actually has already been discussed in the thread. I’m sorry to repeat.
So changing it back to non-passive won’t break anything. It’s a good chance to make the change.
The “workaround” requires users to search the issue tracker (and in “closed” category now), at least it should be in the documentation.
Ok, read this whole thread, didn’t find what I was hoping to find: away to tell React to NOT use passive event on a specific element, or at least some way to tell React to bind a specific event on that element instead of on the root element (as a delegated event)
So, I want to stop the page from scrolling while the wheel (mouse for that matter) is being used over a specific range input field (
<input type="range"/>) and I hacked it like so:https://stackoverflow.com/a/65795791/104380
Here’s my copied answer from stackoverflow:
Where
onWheelis a callback for<input type="range" wheel={onWheel} />All works perfectly now. @yspektor solution is great but my fault was I put the code in componentDidMount not in
indexwhere is invokedReactDom.Render()Cheers 👍This Chrome update is causing us problems. I’m building a react component library that is intended to be used by the general public, so polluting the global scope by modifying native methods on the document object is probably not going to be a valid option for us. Unfortunately resorting to native listeners doesn’t work for us either since some of our components are using portals and native events don’t propagate up the virtual dom through portals the same way that React events do.
Any suggestions?
How long will it take for the issue to be fixed? What is the priority? None of the workarounds is 100% acceptable. Writing libraries using native events will be cumbersome for plenty of reasons. I have just faced the same issue as @Spenc84
@byronwall I can see how websites are broken anyway be it requiring the manual addition of
{ passive: false }or updating React version and rebuilding the bundle: any solution requires equal developer intervention. And in many cases the devs are long gone and no one knows how stuff was built or works. So it’s a really bizarre situation. I guess it better be the{ passive: false }fix then instead of React fixing Chrome stuff. I agree that Chrome team did indeed break some part of the internet just because they like it more: they’ve simply grown too confident in their software monopoly. Go Firefox, what can I say.@cherscarlett
One must not question the existence of God because one’s mind is vanishingly small compared to the mind of God and so one can’t possibly ever grasp a hint of His divine majesty. If God tells you do something you must do that without questioning or hesitation, otherwise you’re a heretic and must be judged by the Holy Inquisition and later bunt alive to clean your soul of the sins and earn forgiveness because God loves all of his children.