bassman21
bassman21

Reputation: 390

How can I get "useState" to work properly when using event handlers for mouse events witin a React component?

I am trying to create a simple "wrapper" to make HTML elements "draggable".

The problem I am having is that the "useState" variable draggable is always false within the event handler mouseMove. The event handlers mouseDown and mouseUp seem to work fine, except the fact that dragging is not properly set to true.

What am I doing wrong? Every suggestion or hint is highly appreciated!!!

PS: console.log('Dragging is set to true') and console.log('Dragging is set to false') are working fine! As far as understand it, all events "fire" properly. It's just that dragging is always false within mouseMove.

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

interface Coordinate {
  x: number;
  y: number;
}

const SimpleDraggableWrapper = (props: any) => {
  const [offset, setOffset] = useState<Coordinate | null>(null);
  const [dragging, setDragging] = useState<boolean>(false);
  const element = useRef<HTMLDivElement>(null);

  const mouseDown = (event: React.MouseEvent) => {
    if (element.current) {
      setOffset({
        x: event.clientX - element.current.offsetLeft,
        y: event.clientY - element.current.offsetTop,
      });
      setDragging(true);
      console.log('Dragging set to true');
    }
  };

  const mouseUp = (event: MouseEvent) => {
    setDragging(false);
    console.log('Dragging set to false');
  };

  const mouseMove = (event: MouseEvent) => {
    console.log(dragging);
    if (dragging && element.current && offset) {
      element.current.style.left = `${event.clientX - offset.x}px`;
      element.current.style.top = `${event.clientY - offset.y}px`;
    }
  };

  useEffect(() => {
    document.addEventListener('mouseup', mouseUp);
    document.addEventListener('mousemove', mouseMove);
    return () => {
      document.removeEventListener('mouseup', mouseUp);
      document.removeEventListener('mousemove', mouseMove);
    };
  }, []);

  return (
    <div ref={element} onMouseDown={mouseDown}>
      {React.cloneElement(props.children)}
    </div>
  );
};

export default SimpleDraggableWrapper;

Upvotes: 0

Views: 1103

Answers (1)

Zakaria
Zakaria

Reputation: 155

This is a common problem related to stale closures.

To know more check: https://dmitripavlutin.com/react-hooks-stale-closures/.

You can resolve this issue in two different ways:

  1. Add dragging in the useEffect dependency array
    useEffect(() => {
      document.addEventListener('mouseup', mouseUp);
      return () => {
        document.removeEventListener('mouseup', mouseUp);
      };
    }, []);
    useEffect(() => {
      document.addEventListener('mousemove', mouseMove);
      return () => {
        document.removeEventListener('mousemove', mouseMove);
      };
    }, [dragging]);
  1. Wrap mouseMove in a useCallback hook with dragging in its dependency array and then add mouseMouve function in the useEffect dependency array
    const mouseMove = useCallback((event: MouseEvent) => {
      console.log(dragging);
      if (dragging && element.current && offset) {
        element.current.style.left = `${event.clientX - offset.x}px`;
        element.current.style.top = `${event.clientY - offset.y}px`;
      }
    }, [dragging]);
    
    useEffect(() => {
      document.addEventListener('mouseup', mouseUp);
      return () => {
        document.removeEventListener('mouseup', mouseUp);
      };
    }, []);
    useEffect(() => {
      document.addEventListener('mousemove', mouseMove);
      return () => {
        document.removeEventListener('mousemove', mouseMove);
      };
    }, [mouseMove]);

Upvotes: 1

Related Questions