2

I am fairly new to hooks and I am trying to implement a drag and drop container component that handles onDragStart, onDrag and onDragEnd functions throughout the mouse movement. I have been trying to replicate the code found here using hooks : https://medium.com/@crazypixel/mastering-drag-drop-with-reactjs-part-01-39bed3d40a03

I have almost got it working using the code below. It is animated using styled components. The issue is it works only if you move the mouse slowly. If you move the mouse quickly the SVG or whatever is contained in this div is thrown of the screen.

I have a component.js file that looks like

import React, { useState, useEffect, useCallback } from 'react';
import { Container } from './style'

const Draggable = ({children, onDragStart, onDrag, onDragEnd, xPixels, yPixels, radius}) => {
  const [isDragging, setIsDragging] = useState(false);
  const [original, setOriginal] = useState({
    x: 0,
    y: 0
  });
  const [translate, setTranslate] = useState({
    x: xPixels,
    y: yPixels
  });
  const [lastTranslate, setLastTranslate] = useState({
    x: xPixels,
    y: yPixels
  });

  useEffect(() =>{
    setTranslate({
      x: xPixels,
      y: yPixels
    });
    setLastTranslate({
      x: xPixels,
      y: yPixels
    })
  }, [xPixels, yPixels]);

  const handleMouseMove = useCallback(({ clientX, clientY }) => {

    if (!isDragging) {
      return;
    }
    setTranslate({
      x: clientX - original.x + lastTranslate.x,
      y: clientY - original.y + lastTranslate.y
    });
  }, [isDragging, original,  lastTranslate, translate]);



  const handleMouseUp = useCallback(() => {
    window.removeEventListener('mousemove', handleMouseMove);
    window.removeEventListener('mouseup', handleMouseUp);

    setOriginal({
      x:0,
      y:0
    });
    setLastTranslate({
      x: translate.x,
      y: translate.y
    });

    setIsDragging(false);
    if (onDragEnd) {
      onDragEnd();
    }

  }, [isDragging, translate, lastTranslate]);

  useEffect(() => {
    window.addEventListener('mousemove', handleMouseMove);
    window.addEventListener('mouseup', handleMouseUp);

    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
      window.removeEventListener('mouseup', handleMouseUp)
    };
  }, [handleMouseMove, handleMouseUp]);

  const handleMouseDown = ({ clientX, clientY }) =>{

    if (onDragStart) {
      onDragStart();
    }
    setOriginal({
      x: clientX,
      y: clientY
    });
    setIsDragging(true);
  };

  return(
    <Container
      onMouseDown={handleMouseDown}
      x={translate.x}
      y={translate.y}
      {...{radius}}
      isDragging={isDragging}
    >
      {children}
    </Container>
  )
};

export default Draggable

And the styled components file, styled.js looks like the following:

import styled from 'styled-components/macro';

const Container = styled.div.attrs({
  style: ({x,y, radius}) => ({
    transform: `translate(${x - radius}px, ${y - radius}px)`
  })
})`
  //cursor: grab;
  position: absolute;

  ${({isDragging}) =>
    isDragging && `

    opacity: 0.8
    cursor: grabbing
  `}
`;

export {
  Container
}

So I pass in the initial value from the parent initially. I think i am not dealing with the useEffect / useState correctly and it is not getting the information fast enough.

I would be extremely grateful if someone can help me figure out how to fix this issue. Apologies again, but I am very new to using hooks.

Thanks You :)

skyboyer
  • 22,209
  • 7
  • 57
  • 64
mineshmshah
  • 430
  • 2
  • 7
  • 16
  • Can you make a CodeSandbox? It would be easier to debug. – Colin Ricardo May 07 '19 at 00:10
  • Here is the codesandbox ... it doesnt seem to be as smooth as when i do it on storybook and the increments seem to be too big :S https://codesandbox.io/s/81jp9owqp2 – mineshmshah May 07 '19 at 00:33
  • Added a fix to the make the numbers are added correctly. If you try and almost flick the mouse and move it quickly the rounded div just moves way off screen – mineshmshah May 07 '19 at 00:56

1 Answers1

9

Ideally, since setState is asynchronous you'd move all your state into one object (as the medium example does). Then, you can leverage the setState callback to make sure the values that each event listener and event callback is using are up-to-date when setState is called.

I think the example in that medium article had the same jumping issue (which is probably why the example video moved the objects slowly), but without a working example, it's hard to say. That said, to resolve the issue, I removed the originalX, originalY, lastTranslateX, lastTranslateY values as they're not needed since we're leveraging the setState callback.

Furthermore, I simplified the event listeners/callbacks to:

  • mousedown => mouse left click hold sets isDragging true
  • mousemove => mouse movement updates translateX and translateY via clientX and clientY updates
  • mouseup => mouse left click release sets isDragging to false.

This ensures that only one event listener is actually transforming x and y values.

If you want to leverage this example to include multiple circles, then you'll need to either reuse the component below OR use useRef and utilize the refs to move the circle that is selected; however, that's beyond the scope of your original question.

Lastly, I also fixed a styled-components deprecation issue by restructuring the styled.div.data.attr to be a function that returns a style property with CSS, instead of an object with a style property that is a function that returns CSS.

Deprecated:

styled.div.attrs({
  style: ({ x, y, radius }) => ({
    transform: `translate(${x - radius}px, ${y - radius}px)`
  })
})`

Updated:

styled.div.attrs(({ x, y, radius }) => ({
  style: {
    transform: `translate(${x - radius}px, ${y - radius}px)`
  }
}))`

Working example:

Edit Drag and Drop Example


components/Circle

import styled from "styled-components";

const Circle = styled.div.attrs(({ x, y, radius }) => ({
  style: {
    transform: `translate(${x - radius}px, ${y - radius}px)`
  }
}))`
  cursor: grab;
  position: absolute;
  width: 25px;
  height: 25px;
  background-color: red;
  border-radius: 50%;

  ${({ isDragging }) =>
    isDragging &&
    `
    opacity: 0.8;
    cursor: grabbing;
  `}
`;

export default Circle;

components/Draggable

import React, { useState, useEffect, useCallback } from "react";
import PropTypes from "prop-types";
import Circle from "../Circle";

const Draggable = ({ position, radius }) => {
  const [state, setState] = useState({
    isDragging: false,
    translateX: position.x,
    translateY: position.y
  });

  // mouse move
  const handleMouseMove = useCallback(
    ({ clientX, clientY }) => {
      if (state.isDragging) {
        setState(prevState => ({
          ...prevState,
          translateX: clientX,
          translateY: clientY
        }));
      }
    },
    [state.isDragging]
  );

  // mouse left click release
  const handleMouseUp = useCallback(() => {
    if (state.isDragging) {
      setState(prevState => ({
        ...prevState,
        isDragging: false
      }));
    }
  }, [state.isDragging]);

  // mouse left click hold
  const handleMouseDown = useCallback(() => {
    setState(prevState => ({
      ...prevState,
      isDragging: true
    }));
  }, []);

  // adding/cleaning up mouse event listeners
  useEffect(() => {
    window.addEventListener("mousemove", handleMouseMove);
    window.addEventListener("mouseup", handleMouseUp);

    return () => {
      window.removeEventListener("mousemove", handleMouseMove);
      window.removeEventListener("mouseup", handleMouseUp);
    };
  }, [handleMouseMove, handleMouseUp]);

  return (
    <Circle
      isDragging={state.isDragging}
      onMouseDown={handleMouseDown}
      radius={radius}
      x={state.translateX}
      y={state.translateY}
    />
  );
};

// prop type schema
Draggable.propTypes = {
  position: PropTypes.shape({
    x: PropTypes.number,
    y: PropTypes.number
  }),
  radius: PropTypes.number
};

// default props if none are supplied
Draggable.defaultProps = {
  position: {
    x: 20,
    y: 20
  },
  radius: 10,
};

export default Draggable;
Matt Carlotta
  • 18,972
  • 4
  • 39
  • 51