Reputation: 480
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 :)
Upvotes: 2
Views: 5008
Reputation: 19762
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
updatesmouseup
=> 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:
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;
Upvotes: 9