Search code examples
javascriptreactjstypescriptpreact

Custom, Draggable component too slow in ReactJS when Using onMouseMove()


I am trying to create a draggable preact component. My current implementation breaks if the cursor moves to fast. Here is the code.

export { Draggable };

import { h } from "preact";
import { useState } from "preact/hooks";

const Draggable = (props: any) => {
    const [styles, setStyles] = useState({});
    const [diffPos, setDiffPos] = useState({ diffX: 0, diffY: 0 });
    const [isDragging, setIsDragging] = useState(false);

    const dragStart = (e: MouseEvent): void => {
        const boundingRect =
            (e.currentTarget as HTMLElement).getBoundingClientRect();

        setDiffPos({
            diffX: e.screenX - boundingRect.left,
            diffY: e.screenY - boundingRect.top,
        });

        setIsDragging(true);
    }

    const dragging = (e: MouseEvent): void => {
        if (isDragging === true) {
            const left = e.screenX - diffPos.diffX;
            const top = e.screenY - diffPos.diffY;

            setStyles({ left: left, top: top });
        }
    }

    const dragEnd = (): void => {
        setIsDragging(false);
    }

    return (
        <div
            class="draggable"
            style={{ ...styles, position: "absolute" }}
            onMouseDown={dragStart}
            onMouseMove={dragging}
            onMouseUp={dragEnd}
        >
            {props.children}
        </div>
    );
}

I tried to fix it by creating a mouseup event listener but the element stops dragging if I move the mouse to fast.

Here is my attempted fix:

export { Draggable };

import { h } from "preact";
import { useState } from "preact/hooks";

const Draggable = (props: any) => {
    const [styles, setStyles] = useState({});
    const [diffPos, setDiffPos] = useState({ diffX: 0, diffY: 0 });
    const [isDragging, setIsDragging] = useState(false);

    const dragStart = (e: MouseEvent): void => {
        const boundingRect =
            (e.currentTarget as HTMLElement).getBoundingClientRect();

        setDiffPos({
            diffX: e.screenX - boundingRect.left,
            diffY: e.screenY - boundingRect.top,
        });

        setIsDragging(true);

        // ------------------------------------------------------------ Added an event listener
        document.addEventListener("mouseup", dragEnd, { once: true });
    }

    const dragging = (e: MouseEvent): void => {
        if (isDragging === true) {
            const left = e.screenX - diffPos.diffX;
            const top = e.screenY - diffPos.diffY;

            setStyles({ left: left, top: top });
        }
    }

    const dragEnd = (): void => {
        setIsDragging(false);
    }

    return (
        <div
            class="draggable"
            style={{ ...styles, position: "absolute" }}
            onMouseDown={dragStart}
            onMouseMove={dragging}
            // -------------------------------------------------------- Removed onMouseUp
        >
            {props.children}
        </div>
    );
}

Solution

  • The problem is that onMouseMove() triggers every time you move the mouse, so, if you move over 200 pixels very slowly, that's 200 iterations. Try instead using onDragStart and onDragEnd. Full working demo.

    Ultimately, your change will be here in render()...

     return (
       <div
         class="draggable"
         style={{ ...styles, position: "absolute" }}
         onDragStart={(e) => dragStart(e)}
         onDragEnd={(e) => dragging(e)}
         draggable={true}
       >
         {props.children}
       </div>
     );
    

    I use dragEnd(), so only two events actually fire with dragging: start and end. MouseMove was being fired every time there was movement, which could be hundreds of times during dragging.

    Also, by giving it that extra draggable={true} param, the browser will treat it as a naturally-draggable item (i.e., a semi-opaque version of the dragged item will visually appear at the position of the cursor, as the user drags the element around).

    Finally, to speed up things just a smidge, I removed the eventListener you had for dragEnd in dragStart().