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