React onWheel Handler Can't preventDefault Because It's a Passive Event Listenever
I'm Trying to Override Ctrl+Scroll Behavior on a Component, but It's Not Working with the Error [Intervention] Unable to preventDefault Inside Passive Event...
I'm trying to override Ctrl+scroll behavior on a component, but it's not working with the error [Intervention] Unable to preventDefault inside passive event listener due to target being treated as passive. See <URL>. I think I'm okay with using an active listener, so is there a way to specify that through React? Note that I need to access and modify state within onWheel.
const onWheel = (e: React.WheelEvent): void => {
if (e.altKey) {
e.preventDefault();
// Error
} else if (e.ctrlKey) {
e.preventDefault();
// Error
}
};
...
return (<div className={styles["workspace"]} onWheel={onWheel}>
stuff
</div>);
2 Answers
A bit late, but maybe it helps someone else.
The problem is that React uses passive event handlers by default with wheel, touchstart and touchmove events - in other words, you can't call stopPropagation within them.
If you want to use non-passive event handlers, you need to use refs and add/remove event handler manually, like this:
class MyComponent extends React.Component {
myRef = React.createRef();
componentDidMount() {
// IMPORTANT: notice the `passive: false` option
this.myRef.current.addEventListener('wheel', this.handleWheel, { passive: false });
}
componentWillUnmount() {
this.myRef.current.removeEventListener('wheel', this.handleWheel, { passive: false });
}
handleWheel = (e) => {
e.stopPropagation();
// ...
}
// ...
}
Should be similar with hooks.
For anyone using functional components, here is a solution, taking inspiration from @johndodo
If your onWheel does not change over time
function MyComponent {
const onWheel = useCallback(
(e) => {
console.log(e);
e.preventDefault();
},
[],
);
const divRefCallback = useCallback(
(node) => {
console.log(node);
if (node == null) {
return;
}
node.addEventListener('wheel', onWheel, { passive: false });
},
[onWheel],
);
return <div ref={divRefCallback}>...</div>;
}
The reason to use a useCallback for divRef instead of useRef + useEffect is this.
If your onWheel changes over time
e.g. if it depends on a state.
This requires some extra work because you need to remove the old event listener.
function MyComponent {
const [x, setX] = useState(0);
const onWheel = useCallback(
(e) => {
console.log(e);
console.log(x);
e.preventDefault();
},
[x],
);
const divRef = useRef(null);
const divRefCallback = useCallback(
(node) => {
console.log(node);
if (node == null) {
if (divRef.current != null) {
/* This happens when the <div> component tries to detach the old divRefCallback.
* Since we are inside a `useCallback`, the `onWheel` callback being remove is still the old one here.
*/
divRef.current.removeEventListener('wheel', onWheel, { passive: false });
}
return;
}
divRef.current = node;
node.addEventListener('wheel', onWheel, { passive: false });
},
[onWheel],
);
return <div ref={divRefCallback}>...</div>;
}