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 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>;
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Marcus Vance

Marcus Vance

Cybersecurity & Digital Privacy Researcher

Marcus Vance is a cybersecurity auditor and technology writer dedicated to educating the public about online safety, data privacy regulations, enterprise security, and emerging cyber threats.

Share this article
Twitter Facebook Pinterest