React useState hook event handler using initial state
JavascriptReactjsEcmascript 6React HooksJavascript Problem Overview
I'm still getting my head around react hooks but struggling to see what I'm doing wrong here. I have a component for resizing panels, onmousedown
of an edge I update a value on state then have an event handler for mousemove
which uses this value however it doesn't seem to be updating after the value has changed.
Here is my code:
export default memo(() => {
const [activePoint, setActivePoint] = useState(null); // initial is null
const handleResize = () => {
console.log(activePoint); // is null but should be 'top|bottom|left|right'
};
const resizerMouseDown = (e, point) => {
setActivePoint(point); // setting state as 'top|bottom|left|right'
window.addEventListener('mousemove', handleResize);
window.addEventListener('mouseup', cleanup); // removed for clarity
};
return (
<div className="interfaceResizeHandler">
{resizePoints.map(point => (
<div
key={ point }
className={ `interfaceResizeHandler__resizer interfaceResizeHandler__resizer--${ point }` }
onMouseDown={ e => resizerMouseDown(e, point) }
/>
))}
</div>
);
});
The problem is with the handleResize
function, this should be using the latest version of activePoint
which would be a string top|left|bottom|right
but instead is null
.
Javascript Solutions
Solution 1 - Javascript
useRef
to read future value
Currently, your issue is that you're reading a value from the past. When you define handleResize
it belongs to that render, therefore, when you rerender, nothing happens to the event listener so it still reads the old value from its render.
To fix this, you should use a ref via useRef
that you keep updated so that you can read the current value.
const [activePoint, _setActivePoint] = React.useState(null);
// define a ref
const activePointRef = React.useRef(activePoint);
// in place of original `setActivePoint`
const setActivePoint = x => {
activePointRef.current = x; // keep updated
_setActivePoint(x);
};
const handleResize = () => {
// now when reading `activePointRef.current` you'll
// have access to the current state
console.log(activePointRef.current);
};
const resizerMouseDown = /* still the same */;
return /* return is still the same */
Solution 2 - Javascript
You have access to current state from setter function, so you could make it:
const handleResize = () => {
setActivePoint(activePoint => {
console.log(activePoint);
return activePoint;
})
};
Solution 3 - Javascript
const [activePoint, setActivePoint] = useState(null); // initial is null
const handleResize = () => {
setActivePoint(currentActivePoint => { // call set method to get the value
console.log(currentActivePoint);
return currentActivePoint; // set the same value, so nothing will change
// or a different value, depends on your use case
});
};
Solution 4 - Javascript
For those using typescript, you can use this function:
export const useReferredState = <T>(
initialValue: T = undefined
): [T, React.MutableRefObject<T>, React.Dispatch<T>] => {
const [state, setState] = useState<T>(initialValue);
const reference = useRef<T>(state);
const setReferredState = (value) => {
reference.current = value;
setState(value);
};
return [state, reference, setReferredState];
};
And call it like that:
const [ recordingState, recordingStateRef, setRecordingState, ] = useReferredState<{ test: true }>();
and when you call setRecordingState
it will automatically update the ref and the state.
Solution 5 - Javascript
Just small addition to the awe ChrisBrownie55's advice.
A custom hook can be implemented to avoid duplicating this code and use this solution almost the same way as the standard useState
:
// useReferredState.js
import React from "react";
export default function useReferredState(initialValue) {
const [state, setState] = React.useState(initialValue);
const reference = React.useRef(state);
const setReferredState = value => {
reference.current = value;
setState(value);
};
return [reference, setReferredState];
}
// SomeComponent.js
import React from "react";
const SomeComponent = () => {
const [someValueRef, setSomeValue] = useReferredState();
// console.log(someValueRef.current);
};
Solution 6 - Javascript
useRef
for the callback
Beside the correct way suggested by ChrisBrownie55, you can have a similar approach, which might be easier to maintain, by using useRef
for the eventListener's callback itself, instead of the useState
's value.
In this way you shouldn't be worried about saving in a reference every useState
you would like to use in the future.
Just save the handleResize
in a ref and update its value on every render:
const handleResizeRef = useRef(handleResize)
handleResizeRef.current = handleResize;
and use the handleResizeRef
as a callback, wrapped in an arrow function:
window.addEventListener('mousemove', e => handleResizeRef.current(e));
Sandbox example
https://codesandbox.io/s/stackoverflow-55265255-answer-xe93o?file=/src/App.js
Full code using custom hook:
/*
this custom hook creates a ref for fn, and updates it on every render.
The new value is always the same fn,
but the fn's context changes on every render
*/
const useRefEventListener = fn => {
const fnRef = useRef(fn);
fnRef.current = fn;
return fnRef;
};
export default memo(() => {
const [activePoint, setActivePoint] = useState(null);
const handleResize = () => {
console.log(activePoint);
};
// use the custom hook declared above
const handleResizeRef = useRefEventListener(handleResize)
const resizerMouseDown = (e, point) => {
setActivePoint(point);
// use the handleResizeRef, wrapped by an arrow function, as a callback
window.addEventListener('mousemove', e => handleResizeRef.current(e));
window.addEventListener('mouseup', cleanup); // removed for clarity
};
return (
<div className="interfaceResizeHandler">
{resizePoints.map(point => (
<div
key={ point }
className={ `interfaceResizeHandler__resizer interfaceResizeHandler__resizer--${ point }` }
onMouseDown={ e => resizerMouseDown(e, point) }
/>
))}
</div>
);
});
Solution 7 - Javascript
You can make use of the useEffect hook and initialise the event listeners every time activePoint changes. This way you can minimise the use of unnecessary refs in your code.