0
votes

I am working on some drag and drop functionality for a web app. Everything works well, but it frequnetly re-renders when moving the dragged element. I understand that it has to re-render in order to update the position, but every part of the component is re-created including the functions. Is there a way to optimize this ?

const Board: React.FC = () => {
  const [dragElement, setDragElement] = useState<HTMLElement | null>(null);
  const [dropElement, setDropElement] = useState<Element | null>(null);
  const [offsets, setOffsets] = useState<{ xOffset: number; yOffset: number }>({
    xOffset: 0,
    yOffset: 0,
  });

  useEffect(() => {
    window.addEventListener("mousedown", dragStart);
    window.addEventListener("mouseup", dragEnd);
    window.addEventListener("mousemove", dragMove);
    window.addEventListener("touchstart", dragStart, { passive: false });
    window.addEventListener("touchend", dragEnd, { passive: false });
    window.addEventListener("touchmove", dragMove, { passive: false });
    return () => {
      window.removeEventListener("mousedown", dragStart);
      window.removeEventListener("mouseup", dragEnd);
      window.removeEventListener("mousemove", dragMove);
      window.removeEventListener("touchstart", dragStart);
      window.removeEventListener("touchend", dragEnd);
      window.removeEventListener("touchmove", dragMove);
    };
  }, [dragElement, dropElement]);

  function dragStart(ev: Event) {
    ev.preventDefault();
    const target = (ev.target as HTMLElement)?.closest(
      `.${classes.ship}`
    ) as HTMLElement;
    if (!target) {
      return;
    }
    const pageX = (ev as MouseEvent).pageX;
    const pageY = (ev as MouseEvent).pageY;
    const clientX =
      (ev as MouseEvent).clientX || (ev as TouchEvent).targetTouches[0].pageX;
    const clientY =
      (ev as MouseEvent).clientY || (ev as TouchEvent).targetTouches[0].pageY;
    const { xOffset, yOffset } = getOffsets(clientX, clientY, target);
    setOffsets({ xOffset, yOffset });
    target.classList.add(classes["clone-selected"]);
    moveElement(pageX - xOffset, pageY - yOffset, target);
    setDragElement(target);
  }

  function dragEnd(ev: Event) {
    ev.preventDefault();
    if (dragElement) {
      dragElement.classList.remove(classes["clone-selected"]);

      if (dropElement) {
        dropElement.appendChild(dragElement);
      }
      setDragElement(null);
    }
  }

  function dragMove(ev: Event) {
    ev.preventDefault();
    if (!dragElement) {
      return;
    }
    const pageX =
      (ev as MouseEvent).pageX || (ev as TouchEvent).targetTouches[0].pageX;
    const pageY =
      (ev as MouseEvent).pageY || (ev as TouchEvent).targetTouches[0].pageY;
    const { xOffset, yOffset } = offsets;
    moveElement(pageX - xOffset, pageY - yOffset, dragElement);
    dragElement.hidden = true;
    const bottomElement = document.elementFromPoint(pageX, pageY);
    dragElement.hidden = false;
    if (!bottomElement) {
      return;
    }
    const dropTarget = bottomElement.closest(`.${classes.dropzone}`);
    dropElement?.classList.remove(classes["cell-hover"]);
    dropTarget?.classList.add(classes["cell-hover"]);
    setDropElement(dropTarget);
  }

  function moveElement(x: number, y: number, element: HTMLElement) {
    element.style.left = x + "px";
    element.style.top = y + "px";
  }

  function getOffsets(x: number, y: number, element: HTMLElement) {
    const rect = element.getBoundingClientRect();
    const xOffset = x - rect.left;
    const yOffset = y - rect.top;
    return { xOffset, yOffset };
  }

...
}