No stable way to get current state of component when using hooks
See original GitHub issueBecause the identity of the value returned by returned by the useState
hook is not stable between renders, some tasks that were easy with class components are challenging or impossible with hooks.
For example, here is a simplified example of a component that can be clicked and dragged, written first as a class component and second as a functional component with hooks:
class DraggableClass extends React.Component {
state = {
isDragging: false,
position: [0, 0],
};
handleMouseMove = event => {
if (this.state.isDragging) {
const newX = this.state.position[0] + event.movementX;
const newY = this.state.position[1] + event.movementY;
this.setState({ position: [newX, newY] });
}
};
handleMouseUp = () => {
this.setState({ isDragging: false });
};
componentDidMount() {
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
}
componentWillUnmount() {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
}
render() {
return (
<div
style={{
position: 'absolute',
left: this.state.position[0],
top: this.state.position[1],
}}
onMouseDown={ () => this.setState({ isDragging: true }) }
>
Drag me!
</div>
);
}
}
function DraggableHooks() {
const [isDragging, setIsDragging] = useState(false);
const [position, setPosition] = useState([0, 0]);
useEffect(() => {
function handleMouseMove(event) {
if (isDragging) {
const newX = position[0] + event.movementX;
const newY = position[1] + event.movementY;
setPosition([newX, newY]);
}
}
window.addEventListener('mousemove', handleMouseMove);
function handleMouseUp() {
setIsDragging(false);
}
window.addEventListener('mouseup', handleMouseUp);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
}
}, []);
return (
<div
style={{
position: 'absolute',
left: position[0],
top: position[1],
}}
onMouseDown={ () => setIsDragging(true) }
>
Drag me!
</div>
);
}
The hooks version does not work because the isDragging
and position
variables within handleMouseMove
are stuck with the old identities of both variables, and never see any new values. The observed behavior in this case is that if (isDragging)
always evaluates to false, even though the value of isDragging in state does change.
I suspect that the fix for this isn’t as simple as “just make the state value stable like setState
”, since I’m guessing that would mess with async rendering. But perhaps something like the following could help:
// This advanced hook would have a stable identity for both value and setValue.
const [value, setValue] = useStableState(defaultValue);
// This would add a third return value to useState which has a stable identity.
const [value, setValue, getValue] = useState(defaultValue);
I am currently using something like the latter in my own code, using refs to persist values for me:
export function useStableState(defaultValue) {
const [value, setValue] = useState(defaultValue);
const valueRef = useRef(defaultValue);
return [
value,
newValue => {
if (typeof newValue === 'function') {
newValue = newValue(valueRef.current);
}
valueRef.current = newValue;
setValue(newValue);
},
() => valueRef.current,
];
}
Issue Analytics
- State:
- Created 4 years ago
- Reactions:1
- Comments:6 (1 by maintainers)
For future readers, I think it’s worth pointing out that OP’s example is missing dependencies in the
useEffect
implementation which is why it didn’t work. I’m unconvinced thatuseRef
in this example is any better thanuseState
. Thank you for the discussion, OP. I’ve consolidated the 3 examples here: https://codesandbox.io/s/react-state-classes-usestate-useref-moyuzOh interesting, I hadn’t considered doing things that way. This still feels to me like a sharp edge that could cause a lot of frustration - maybe at the very least that FAQ could be updated to include an example of the pattern you showed me.
Thanks for looking into this!