React useState hook event handler using initial state

JavascriptReactjsEcmascript 6React Hooks

Javascript 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.

Example (link to jsfiddle):

  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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionSimon StatonView Question on Stackoverflow
Solution 1 - JavascriptAndriaView Answer on Stackoverflow
Solution 2 - JavascriptmachnickiView Answer on Stackoverflow
Solution 3 - JavascriptDerek LiangView Answer on Stackoverflow
Solution 4 - Javascriptjohannb75View Answer on Stackoverflow
Solution 5 - JavascriptNikitaView Answer on Stackoverflow
Solution 6 - JavascriptDavide CantelliView Answer on Stackoverflow
Solution 7 - JavascriptVignesh KesavanView Answer on Stackoverflow