68

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.

Andry
  • 16,172
  • 27
  • 138
  • 246
Simon Staton
  • 4,345
  • 4
  • 27
  • 49
  • Whats the value of point if you do `console.log(point)`, just above `setActivePoint(point)`. I am wondering if `resizerMouseDown` it's even called. – Nicholas Mar 20 '19 at 16:11
  • It would seem that your `.addEventListener(` calls are using the current functions in that render, therefore, when you change `activePoint` and rerender those event listeners are not updated to point to the new `activePoint` – Andria Mar 20 '19 at 16:19

8 Answers8

99

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.

Andria
  • 4,712
  • 2
  • 22
  • 38
  • 1
    this solution is too noisy. I like to use the setActivePoint method instead (see other answers) – Derek Liang Mar 05 '20 at 07:09
  • 1
    @derekliang the OP doesn't want to `setActivePoint` on `handleResize` tho. That would cause way too many updates to the screen. This way, `setActivePoint` is only called once on `resizerMouseDown` but the logging in `handleResize` is independent of re-renders. – Andria Mar 05 '20 at 12:41
  • If the state don't change, it will not update the screen. It will cause the state change check but fail to see the change, therefore there is no update to the screen. – Derek Liang Mar 05 '20 at 22:09
  • 1
    I misspoke, I meant that it would force an update in React. While React knows not to rerender to the DOM, it is still costly to perform hundreds/thousands of checks to see if nothing has changed yet since `handleResize` is attached to the `mousemove` event. – Andria Mar 05 '20 at 22:34
  • What not make use of the useCallback hook? This should prevent stale event handlers? – Joseph King May 09 '20 at 14:32
  • You're right, however, to prevent stale event handlers you have to constantly create new event handlers and mousemove causes an insane amount of updates so it's not feasible to use `useCallback` and stay performant. – Andria May 10 '20 at 01:13
  • @ChrisBrownie55 The documentation for useState states "If your update function returns the exact same value as the current state, the subsequent rerender will be skipped completely.". In practice, when calling setState without changing the state, the outcome is that the component renders partially once (without re-rendering children) to verify that nothing in the component output changed. If nothing changed, subsequent setState calls without modifying the state cause no additional internal renders. – voneiden Oct 15 '20 at 09:04
  • @voneiden What's your concern about? – Andria Oct 16 '20 at 02:37
  • @ChrisBrownie55 concerning the earlier statement that "it would force an update in React" and that "it is still costly to perform hundreds/thousands checks". So my concern is mainly that while the performance penalty is definitely greater with setState, I'm not convinced it is significantly so under normal operating conditions (meaning if the setState bails out from updating rerendering correctly). Of course, that's just a guess on my behalf, since I haven't run any actual perf tests comparing the two methods. – voneiden Oct 16 '20 at 12:09
  • @voneiden I think at this point it might as well just be preference. I personally believe it's easier to reason about when you know that a ref is being used. No need to use your `setState` as a way to get React to give you the **current** state when you can just `useRef` IMO – Andria Oct 16 '20 at 14:36
  • @ChrisBrownie55 Sure, fully agreed. My intent wasn't to dispute the answer, but simply to question the comment related to the perf impact of using setState. Sorry for being unclear on the matter. – voneiden Oct 17 '20 at 18:02
  • 2
    I have the same problem as the OP, but multiplied. I have several event handlers that use several different pieces of state, which call several other internal functions. This is on a component that was formerly class-based that I'm switching over to functional because I want `useEffect`. Anyway, the whole situation feels very buggy to me. It's surprising that a piece of state might be able to get an old value at all. Is the `useRef` + `statePiece.current` fix still the best solution? – Mike Willis Mar 02 '21 at 15:08
  • Hello, I have tried this and this does not update ```activePoint```. However, ```activePointRef``` updates... Do you happen to know why this might be the case? – jeff Aug 08 '22 at 21:15
  • All of this keeping duplicates of every state variable method is hogwash. @Davide Cantelli has the best answer to this problem. I highly suggest everyone use that instead and upvote it so it surpasses this one. – 55 Cancri Sep 17 '22 at 14:34
  • @55Cancri The answer you mention is keeping duplicates of every function; it's very similar to my answer, but it focuses on getting an up-to-date function context instead of an up-to-date variable. That solution is clearly better suited for using many `useState` variables inside one function instead of one `useState` variable across many functions. The best solution varies per person and situation, and my solution is sufficient for the presented problem, which is evident by the OP accepting it. – Andria Sep 19 '22 at 16:10
  • @jeff That's how it's supposed to work. You would use `activePointRef.current` to access the most up-to-date value of `activePoint` from a function with a stale context. – Andria Sep 19 '22 at 16:14
  • @MikeWillis There are other solutions you can try if this one doesn't suit your needs best. If your solution seems to involve a lot of redundancy in your codebase, I would suggest writing a custom hook to abstract away that redundancy; there should be an answer below that does exactly that. – Andria Sep 19 '22 at 16:19
  • 1
    Thanks @Andria. I think I used the ref fix for quite a while. But lately, as I've gotten more familiar with React, I've found I need to do this sort of thing less and less. Can't quite explain what changed though. Maybe I'm using `useEffect` more – Mike Willis Sep 19 '22 at 20:27
41

You have access to current state from setter function, so you could make it:

const handleResize = () => {
  setActivePoint(activePoint => {
    console.log(activePoint);
    return activePoint;
  })
};
machnicki
  • 491
  • 5
  • 3
  • 4
    Is there a reason why this isn't a preferred solution over the recognized answer? Very concise. Also why does this work? How does it fix the scoping issue? – aturc Sep 12 '20 at 00:46
  • 4
    @aturc setState supports functional updates: if a function is passed to setState it receives the current state as the first argument. The function must return a new state. If the new state is the same as the old state, render is skipped. So it can be utilized to do side effects. https://reactjs.org/docs/hooks-reference.html#functional-updates – voneiden Oct 15 '20 at 07:59
  • 1
    Not sure if this is good solution. setState is not supposed to run arbitrary code. – Nitin Jadhav Feb 25 '21 at 14:16
  • After having used the `ref` method (defining your own state-setter function that also updates a ref), I'm starting to prefer this method instead, specifically in functional components with `useEffect` (or similar hooks that have a dependency array). Mainly because if you use the `ref` method with any `useEffects`, you end up having to pass your setter function into the `useEffect`, which I find irritating. – Mike Willis Aug 23 '21 at 13:51
13

useRef for the callback

A similar approach to Andria's can be taken by using useRef to update the event listener's callback itself instead of the useState value. This allows you to use many up-to-date useState values inside one callback with only one useRef.

If you create a ref with useRef and update its value to the handleResize callback on every render, the callback stored in the ref will always have access to up-to-date useState values, and the handleResize callback will be accessible to any stale callbacks like event handlers.

function handleResize() {
  console.log(activePoint);
}

// Create the ref,
const handleResizeRef = useRef(handleResize);
// and then update it on each re-render.
handleResizeRef.current = handleResize;

// After that, you can access it via handleResizeRef.current like so
window.addEventListener("mousemove", event => handleResizeRef.current());

With this in mind, we can also abstract away the creation and updating of the ref into a custom hook.

Example

See it live on CodeSandbox.

/**
 * A custom hook that creates a ref for a function, and updates it on every render.
 * The new value is always the same function, but the function's context changes on every render.
 */
function useRefEventListener(fn) {
  const fnRef = useRef(fn);
  fnRef.current = fn;
  return fnRef;
}

export default memo(() => {
  const [activePoint, setActivePoint] = useState(null);

  // We can use the custom hook declared above
  const handleResizeRef = useRefEventListener((event) => {
    // The context of this function will be up-to-date on every re-render.
    console.log(activePoint);
  });

  function resizerMouseDown(event, point) {
    setActivePoint(point);

    // Here we can use the handleResizeRef in our event listener.
    function handleResize(event) {
      handleResizeRef.current(event);
    }
    window.addEventListener("mousemove", handleResize);

    // cleanup removed for clarity
    window.addEventListener("mouseup", cleanup);
  }

  return (
    <div className="interfaceResizeHandler">
      {resizePoints.map((point) => (
        <div
          key={point}
          className={`interfaceResizeHandler__resizer interfaceResizeHandler__resizer--${point}`}
          onMouseDown={(event) => resizerMouseDown(event, point)}
        />
      ))}
    </div>
  );
});
Andria
  • 4,712
  • 2
  • 22
  • 38
Davide Cantelli
  • 131
  • 1
  • 4
  • 1
    Are you sure this works? I tried it with my own code and I was still having the old (initial?) value – user2078023 Feb 10 '21 at 14:31
  • @user2078023 there was a typo sorry, "handleResizeRef" was "hanleResizeRef". Please check the code working in this sandbox https://codesandbox.io/s/stackoverflow-55265255-answer-xe93o?file=/src/App.js – Davide Cantelli Feb 11 '21 at 16:03
  • This answer made me laugh so damn hard it just fascinates me how creative and innovative humans can be, and this usually shines most brightly within tightly defined constraints. I haven't even tested to see if it works yet but the idea itself is simply amazing and looks logically sound. – 55 Cancri Sep 17 '22 at 14:18
  • UPDATE: it works. This method is now allowing me to do something that I don't think I've seen anywhere else on the internet: keep multiple video + audio elements insync with a single javascript new Audio() element. Amazing. Thank you so much for your ingenuity and out of the box thinking. – 55 Cancri Sep 17 '22 at 14:31
  • I didn't even remember that I've added this comment nor why I was looking for this solution, but thank you a lot for your kind words. Now I am curious to know this magical thing you have done with audio and video – Davide Cantelli Sep 18 '22 at 21:00
7
  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
    });
  };

Derek Liang
  • 1,142
  • 1
  • 15
  • 22
4

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);
};
Nick
  • 144
  • 1
  • 4
4

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.

johannb75
  • 357
  • 5
  • 16
1

When you need to add event listener on component mount

Use, useEffect() hook

We need to use the useEffect to set event listener and cleanup the same.

The use effect dependency list need to have the state variables which are being used in event handler. This will make sure handler don't access any stale event.

See the following example. We have a simple count state which gets incremented when we click on given button. Keydown event listener prints the same state value. If we remove the count variable from the dependency list, our event listener will print the old value of state.

import { useEffect, useState } from 'react';

function App() {
  const [count, setCount] = useState(0);

  const clickHandler = () => {
    console.log({ count });
    setCount(c => c + 1);
  }


  useEffect(() => {
    document.addEventListener('keydown', normalFunction);

    //Cleanup function of this hook
    return () => {
      document.removeEventListener('keydown', normalFunction);
    }
  }, [count])

  return (
    <div className="App">
      Learn
      <button onClick={clickHandler}>Click me</button>
      <div>{count}</div>
    </div>
  );
}


export default App;
rex
  • 359
  • 1
  • 5
0

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.