I am trying to implement drag and drop in React and using SVG elements. The problem is mouseMove does not get triggered if the user moves the mouse too fast. It basically loses the dragging quite frequently. To solve this I think I need to handle the mouseMove in the parent but not sure how to do this with React. I tried several different approaches to no avail.
I tried addEventListener('mousemove', ...) on the parent using a ref, but the problem is that the clientX is a different coordinate system than the current component. Also, the event handler does not have access to any of the state from the component (event with arrow functions). It maintains a stale reference to any state.
I tried setting the clientX and clientY in a context on the parent and then pulling it in from the DragMe component but it is always undefined the first time around for some strange reason even though I give it a default value.
Here's the code I'm working with:
const DragMe = ({ x = 50, y = 50, r = 10 }) => {
const [dragging, setDragging] = useState(false)
const [coord, setCoord] = useState({ x, y })
const [offset, setOffset] = useState({ x: 0, y: 0 })
const [origin, setOrigin] = useState({ x: 0, y: 0 })
const xPos = coord.x + offset.x
const yPos = coord.y + offset.y
const transform = `translate(${xPos}, ${yPos})`
const fill = dragging ? 'red' : 'green'
const stroke = 'black'
const handleMouseDown = e => {
setDragging(true)
setOrigin({ x: e.clientX, y: e.clientY })
}
const handleMouseMove = e => {
if (!dragging) { return }
setOffset({
x: e.clientX - origin.x,
y: e.clientY - origin.y,
})
}
const handleMouseUp = e => {
setDragging(false)
setCoord({ x: xPos, y: yPos })
setOrigin({ x: 0, y: 0 })
setOffset({ x: 0, y: 0 })
}
return (
<svg style={{ userSelect: 'none' }}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseUp}
>
<circle transform={transform} cx="0" cy="0" r={r} fill={fill} stroke={stroke} />
</svg>
)
}
After much experimentation I was able to addEventListener to the parent canvas. I discovered that I needed to useRef in order to allow the mousemove handler to see the current state. The problem I had before was that the handleParentMouseMove handler had a stale reference to the state and never saw the startDragPos.
This is the solution I came up with. If anyone knows of a way to clean this up it would be much appreciated.
const DragMe = ({ x = 50, y = 50, r = 10, stroke = 'black' }) => {
// `mousemove` will not generate events if the user moves the mouse too fast
// because the `mousemove` only gets sent when the mouse is still over the object.
// To work around this issue, we `addEventListener` to the parent canvas.
const canvasRef = useContext(CanvasContext)
const [dragging, setDragging] = useState(false)
// Original position independent of any dragging. Updated when done dragging.
const [originalCoord, setOriginalCoord] = useState({ x, y })
// The distance the mouse has moved since `mousedown`.
const [delta, setDelta] = useState({ x: 0, y: 0 })
// Store startDragPos in a `ref` so handlers always have the latest value.
const startDragPos = useRef({ x: 0, y: 0 })
// The current object position is the original starting position + the distance
// the mouse has moved since the start of the drag.
const xPos = originalCoord.x + delta.x
const yPos = originalCoord.y + delta.y
const transform = `translate(${xPos}, ${yPos})`
// `useCallback` is needed because `removeEventListener`` requires the handler
// to be the same as `addEventListener`. Without `useCallback` React will
// create a new handler each render.
const handleParentMouseMove = useCallback(e => {
setDelta({
x: e.clientX - startDragPos.current.x,
y: e.clientY - startDragPos.current.y,
})
}, [])
const handleMouseDown = e => {
setDragging(true)
startDragPos.current = { x: e.clientX, y: e.clientY }
canvasRef.current.addEventListener('mousemove', handleParentMouseMove)
}
const handleMouseUp = e => {
setDragging(false)
setOriginalCoord({ x: xPos, y: yPos })
startDragPos.current = { x: 0, y: 0 }
setDelta({ x: 0, y: 0 })
canvasRef.current.removeEventListener('mousemove', handleParentMouseMove)
}
const fill = dragging ? 'red' : 'green'
return (
<svg style={{ userSelect: 'none' }}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
>
<circle transform={transform} cx="0" cy="0" r={r} fill={fill} stroke={stroke} />
</svg>
)
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With