'How to get the mouse position while dragging. Setstate not updating from addEventListener?
My goal: making a custom knob.
I'm trying to build a custom knob that when I click it, and drag it up/down, will increase/decrease in value until the 'mouseup' event and then stay at it's current value.
Currently I'm using an eventlistener for the 'mousemove' action, which works like it should, but the component doesn't update until the 'mouseup' event takes place within the component. What I'm trying to achieve is constant updates as long as you hold the mouse, and updating the moment you release the mouse, even is that's out of the component. I may making some mistake with the timing of setState within an eventlistener, eventlisteners are quite new to me.
My current code:
import React, {useState} from "react";
export default function ClickDrag({}) {
const valueFromMouseDelta = ({x, y}) => x + y; // <- calculate the value from x,y location
const [value, updateValue] = useState(0);
const [startLocation, updateStartLocation] = useState({x: 0, y: 0});
const [relativeLocation, updateRelativeLocation] = useState({x: 0, y: 0});
const handleMouseDown = (e) => {
updateStartLocation({x: e.clientX, y: e.clientY});
window.addEventListener("mousemove", handleDrag);
window.addEventListener("mouseup", () => {
window.removeEventListener("mousemove", handleDrag);
});
};
const handleDrag = (e) => {
updateRelativeLocation({x: e.clientX - startLocation.x, y: startLocation.y - e.clientY}); // <- get the relative location
updateValue(value + valueFromMouseDelta(relativeLocation)); // <- the actual update I want to happen with every move, which doesn't happen
console.log(value, {x: e.clientX - startLocation.x, y: startLocation.y - e.clientY}); // <- the value stays 0 while the x,y location does update nicely
};
return (
<div style={{margin: 10}} onMouseDown={handleMouseDown}>
{value}
</div>);
}
Solution 1:[1]
As @CodeBuggy said, I needed to use a ref value instead of the state. In my case, with a functional component, that meant the useRef()
hook. To make this transition as easy as possible, I wrote this hook around it:
import {useRef} from "react";
export function useRefSet(initialState) {
const ref = useRef(initialState);
const setRef = newValue =>
ref.current = newValue;
const getRef = () =>
ref.current;
return [getRef, setRef]
}
The downside is that the ref
can't be passed to other components this way. For my purposes this is fine, and working with a get and set method is more intuitive to me than a ref where you need to get/set the current value by hand.
Solution 2:[2]
Replace your handle drag with this version and see if it updates:
const handleDrag = (e) => {
const newRelativeLocation = {x: e.clientX - startLocation.x, y: startLocation.y - e.clientY}
updateRelativeLocation(newRelativeLocation);
updateValue(value + valueFromMouseDelta(newRelativeLocation)); // <- update the relative location with newRelativeLocation not relativeLocation
};
The reason its "not" updating is because the relativeLocation
scoped in handleDrag
is still the old relativeLocation
used when you added the function into the event listener.
In other words, value
is updating but the relativeLocation
that you are using when calculating the updated value that is scoped in handleDrag
is still the old value, so there is nothing being changed. Calling updateRelativeLocation
doesn't immediately set the value of relativeLocation
. That would be mutating its value if it did. The relativeLocation
inside handleDrag
will only be updated the next time the component is rendered. What we're doing instead is using the value that will be the next relativeLocation
and using that to calculate the new value.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Michiel |
Solution 2 | Chan Youn |