React - useState - why setTimeout function does not have latest state value?
Asked Answered
S

5

61

Recently I was working on React Hooks and got stuck with one problem/doubt?

Below is a basic implementation to reproduce the issue, Here I'm just toggling flag (a state) variable on click of the button.

  const [flag, toggleFlag] = useState(false);
  const data = useRef(null);
  data.current = flag;

  const _onClick = () => {
    toggleFlag(!flag);
    // toggleFlag(!data.current); // working

    setTimeout(() => {
      toggleFlag(!flag); // does not have latest value, why ?
      // toggleFlag(!data.current); // working
    }, 2000);
  };

  return (
    <div className="App">
      <button onClick={_onClick}>{flag ? "true" : "false"}</button>
    </div>
  );

I figured out some other way to overcome this problem like using useRef or useReducer, but is this correct or is there any other way to solve this with useState only?

Also, it would be really helpful if anyone explains why we get old value of state inside the setTimeout.

Sandbox URL - https://codesandbox.io/s/xp540ynomo

Sumbawa answered 16/3, 2019 at 15:37 Comment(0)
A
41

This boils down to how closures work in JavaScript. The function given to setTimeout will get the flag variable from the initial render, since flag is not mutated.

You could instead give a function as argument to toggleFlag. This function will get the correct flag value as argument, and what is returned from this function is what will replace the state.

Example

const { useState } = React;

function App() {
  const [flag, toggleFlag] = useState(false);

  const _onClick = () => {
    toggleFlag(!flag);

    setTimeout(() => {
      toggleFlag(flag => !flag)
    }, 2000);
  };

  return (
    <div className="App">
      <button onClick={_onClick}>{flag ? "true" : "false"}</button>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>
Armin answered 16/3, 2019 at 15:42 Comment(6)
Doesn't _onClick get a new closure and a new flag value on every render?Lillith
@Lillith Yes, but when _onClick is first invoked the timeout is created, and then when the timeout function is invoked some time later, flag will be looked up in the context of the initial render, so it will get the "old" value.Armin
When does the closure fot setTimeout get created? I thought that it is created every time _onClick is called and gets the flag captured by _onClick and not the original flag variable.Lillith
@Lillith I think it will make sense if you think of App as a function, and every re-render will cause a new call to the App function. The first call to App does not "see" the variables in the second call to App.Armin
May be I got the "initial render" wrong. For me it sounds like setTimeout has flag = false forever as it was on the first App call. Please, check my answer.Lillith
@Lillith Yes, you are right in that the function given to setTimeout in the initial render will always see flag = false.Armin
S
9

If you want to have the up-to-date value of a state variable in setTimeout then you should use refs

const App = () => {
  const messageRef = useRef('');
  const [message, setMessage] = useState('');

  useEffect(() => {
    // THIS IS THE MAGIC PART
    messageRef.current = message;
  }, [message]);

  const handleChange = (e) => {
    e.preventDefault();
    setMessage(e.target.value);
  };

  const sendMessage = (e) => {
    e.preventDefault();
    setTimeout(() => {
      // The most recent value of _message_ variable
      alert(messageRef.current);
    }, 2000);
  };

  return (
    <>
      <input onChange={handleChange} value={message} />
      <button onClick={sendMessage}>
        Send message
      </button>
    </>
  )
}

Read more: https://felixgerschau.com/react-hooks-settimeout/

Sangsanger answered 4/10, 2023 at 8:40 Comment(0)
L
3

The function given to setTimeout will get the flag variable from the _onClick function. The _onClick function gets created every render and "stores" the value which the flag variable gets on this render.

function App() {
  const [flag, toggleFlag] = useState(false);
  console.log("App thinks that flag is", flag);

  const _onClick = () => {
    console.log("_onClick thinks that flag is", flag);
    toggleFlag(!flag);

    setTimeout(() => {
      console.log("setTimeout thinks that flag is", flag);
    }, 100);
  };

  return (
    <div className="App">
      <button onClick={_onClick}>{flag ? "true" : "false"}</button>
    </div>
  );
}

Console:

App thinks that flag is false

_onClick thinks that flag is false
App thinks that flag is true
setTimeout thinks that flag is false

_onClick thinks that flag is true
App thinks that flag is false
setTimeout thinks that flag is true
Lillith answered 16/3, 2019 at 16:32 Comment(0)
G
3

Simply, when you call setTimeout, the function goes with the values you send and put in a queue with the values at the moment you call, and after a certain period of time, they are executed using the values at the moment you call. Instead you can follow the change of the variable with useEffect and get the most current value if you setTimeout in useEffect.

Gibson answered 4/10, 2023 at 9:0 Comment(2)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Ocieock
@Gibson shouldn't it will create closure around the variables being used in callback for setTimout ?Brownie
N
0

You can also use this package created to address this specific "old state values in inner functions" problem: https://github.com/Aminadav/react-useStateRef

Naphthalene answered 20/4, 2023 at 12:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.