useDraggable

React hook to perform an action when an element is dragged.

Implementation

import React, { useState, useEffect, useRef } from "react";

export type Pos = { x: number; y: number };

export const useDraggable = (
  ref: React.MutableRefObject<HTMLDivElement | null>,
  {
    onDrag,
    onDragEnd,
  }: { onDrag: (delta: Pos) => void; onDragEnd?: (delta: Pos) => void },
): {
  isDragging: boolean;
} => {
  const [isDragging, setIsDragging] = useState(false);

  const draggingStart = useRef<Pos | null>(null);
  const mousePos = useRef<Pos | null>(null);

  const getDelta = (): Pos | null =>
    mousePos.current != null && draggingStart.current != null
      ? {
          x: mousePos.current.x - draggingStart.current.x,
          y: mousePos.current.y - draggingStart.current.y,
        }
      : null;

  useEffect(() => {
    const mouseUpListener = () => {
      setIsDragging(false);

      const delta = getDelta();
      if (onDragEnd != null && delta != null) {
        onDragEnd(delta);
      }

      draggingStart.current = null;
      mousePos.current = null;
    };

    window.addEventListener("mouseup", mouseUpListener);
    return () => window.removeEventListener("mouseup", mouseUpListener);
  }, [onDragEnd]);

  useEffect(() => {
    const mouseDownListener = (e: MouseEvent) => {
      draggingStart.current = { x: e.clientX, y: e.clientY };
      setIsDragging(true);
    };

    // Attach event listener to ref after short timeout so we can ensure it is mounted
    // this is a hack to get around the ref in effect problem
    //   -> https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node
    // We can't use a callback ref because we need to remove the event listener when unmounted
    setTimeout(() => {
      ref.current?.addEventListener("mousedown", mouseDownListener);
    }, 1);
    return () =>
      ref.current?.removeEventListener("mousedown", mouseDownListener);
  }, [ref.current]);

  useEffect(() => {
    if (!isDragging) {
      return;
    }

    const mouseMoveListener = (e: MouseEvent) => {
      mousePos.current = { x: e.clientX, y: e.clientY };

      const delta = getDelta();
      if (delta != null) {
        onDrag(delta);
      }
    };

    window.addEventListener("mousemove", mouseMoveListener);
    return () => window.removeEventListener("mousemove", mouseMoveListener);
  }, [isDragging, onDrag]);

  return {
    isDragging,
  };
};

export const useDraggingCursor = (isDragging: boolean, cursor: string) => {
  useEffect(() => {
    if (!isDragging) {
      document.body.style.cursor = "";
      return;
    }

    document.body.style.cursor = cursor;
  }, [isDragging]);
};
Last Updated February 23, 2021