javascriptreactjsecmascript-6react-hooks

React useState hook event handler using initial state


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.


Solution

  • How to Fix a Stale useState

    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.

    There are a several ways to solve this. First let's look at the most simple solution.

    Create your function in scope

    Your event listener for the mouse down event passes the point value to your resizerMouseDown function. That value is the same value that you set your activePoint to, so you can move the definition of your handleResize function into resizerMouseDown and console.log(point). Because this solution is so simple, it cannot account for situations where you need to access your state outside of resizerMouseDown in another context.

    See the in-scope function solution live on CodeSandbox.

    useRef to read a future value

    A more versatile solution would be to create a useRef that you update whenever activePoint changes so that you can read the current value from any stale context.

    const [activePoint, _setActivePoint] = React.useState(null);
    
    // Create a ref
    const activePointRef = React.useRef(activePoint);
    // And create our custom function in place of the original setActivePoint
    function setActivePoint(point) {
      activePointRef.current = point; // Updates the ref
      _setActivePoint(point);
    }
    
    function handleResize() {
      // Now you'll have access to the up-to-date activePoint when you read from activePointRef.current in a stale context
      console.log(activePointRef.current);
    }
    
    function resizerMouseDown(event, point) {
      /* Truncated */
    }
    

    See the useRef solution live on CodeSandbox.

    Addendum

    It should be noted that these are not the only ways to solve this problem, but these are my preferred methods because the logic is more clear to me despite some of the solutions being longer than other solutions offered. Please use whichever solution you and your team best understand and find to best meet your specific needs; don't forget to document what your code does though.