Search code examples
reactjscanvas

React wheel handler to zoom on canvas scrolls page


I have a canvas where I add a background image, but when I zoom it using the onWheel action, the page scrolls as well. When I try to disable that using event.preventDefault(), I get an error:

Unable to preventDefault inside passive event listener invocation

I tried to set passive: false, but no luck.

This is the code I have for the handleWheel:

const SCROLL_SENSITIVITY = 0.0005;
const MAX_ZOOM = 10;
const MIN_ZOOM = 0.3;

const [zoom, setZoom] = useState(0.4);
const [draggind, setDragging] = useState(false);
const canvasRef = useRef(null);
const containerRef = useRef(null);

const clamp = (num, min, max) => Math.min(Math.max(num, min), max);

const handleWheel = (event) => {
    event.preventDefault();
    const { deltaY } = event;
    if (!draggind) {
        setZoom((zoom) =>
            clamp(zoom + deltaY * SCROLL_SENSITIVITY * -1, MIN_ZOOM, MAX_ZOOM)
        );
    }
};

useEffect(() => {
    canvasRef.current.addEventListener('wheel', handleWheel, {passive: false});
    return () => {
        canvasRef.current.removeEventListener('wheel', handleWheel, {passive: false});
    }
}, [handleWheel]);

return (
    <div ref={containerRef}>
        <canvas 
            onWheel={handleWheel}
            ref={canvasRef}
        />
    </div>
);

Solution

  • The error message you are seeing, "Unable to preventDefault inside passive event listener invocation", is related to the use of the passive option in the addEventListener method. When passive is set to true, it tells the browser that the event listener will not call preventDefault() on the event. This is done to improve scrolling performance, as the browser can optimize the scroll event handling.

    To solve your problem, you can remove the passive option from your addEventListener call. You can also remove the onWheel prop from your canvas element, as you are already attaching the event listener to the canvas element in the useEffect hook. Here's the modified code:

    const handleWheel = (event) => {
        event.preventDefault();
        const { deltaY } = event;
        if (!draggind) {
            setZoom((zoom) =>
                clamp(zoom + deltaY * SCROLL_SENSITIVITY * -1, MIN_ZOOM, MAX_ZOOM)
            );
        }
    };
    
    useEffect(() => {
        const canvas = canvasRef.current;
        canvas.addEventListener('wheel', handleWheel);
        return () => {
            canvas.removeEventListener('wheel', handleWheel);
        }
    }, [handleWheel]);
    
    return (
        <div ref={containerRef}>
            <canvas ref={canvasRef} />
        </div>
    );
    
    

    Note that I also assigned the canvasRef.current to a variable to avoid accessing it multiple times in the same block, which can cause performance issues.