tonitone120
tonitone120

Reputation: 2290

Callback doesn't receive the updated version of state even when it fires well after state changes

I'm using a functional component. I'm aware setColor changes the value of color asynchronously. However, my callback function doesn't receive the updated version of color (blue) even when it executes well after color has been updated. Here's an abstract version of my code:

let [color, setColor] = useState("red");

useEffect(() => {
  setColor("blue");
  setTimeout(() => {
    console.log(color);
  }, 5000)
}, []);

(here's a sandbox: https://codesandbox.io/s/falling-cherry-17ip2?file=/src/App.js)

My only guess is that the setColor function almost creates a new color variable & console.log is stuck referencing the old color.


Troubleshooting

  1. I'm aware a secondary useEffect has the potential to execute my callback when state changes. However, this is inconvenient because I'm right in the middle of complex logic where I only want the callback to execute under certain conditions.

  2. I'm also aware useRef variables update pretty much immediately and so that'd be an alternative.


Nevertheless, the question still stands: why isn't the updated value of color being logged & is there anything I could do in the primary useEffect to access the latest version of color state?

Upvotes: 1

Views: 84

Answers (2)

pilchard
pilchard

Reputation: 12919

Here is a snippet logging the color state value at various times and also showing the effect of cleaning up in the return of the useEffect. It only serves to illustrate the timeline that Quentin laid out in their answer.

const App = ()=>{
  const [color, setColor] = React.useState("red");
  
  console.log('outside: ', color);
  
  React.useEffect(() => {
    setColor("blue");
    console.log('inside: ', color);
    setTimeout(() => {
      console.log('inside-timed: ', color);
    }, 5000)
    
    // the cleaned-up timer won't fire
    const timer = setTimeout(() => {
      console.log('timer: ', color);
    }, 5000)

    return (clearTimeout(timer));
  },[])
  
 return (
  <div style={{backgroundColor: color, width: '40px', height: '40px'}} ></div>
 )
}

ReactDOM.render(
  <App />,
  document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>

Upvotes: 1

Quentin
Quentin

Reputation: 943537

  1. setColor changes the state.
  2. The change in stage makes the Function Component execute again
  3. The new invoke of the Function Component reruns let [color, setColor] = useState("red");
  4. This assigns the current state of color to the color variable.
  5. Time passes
  6. The arrow function passed to setTimeout runs. This has closed over the previous color variable which was assigned the old state.

Upvotes: 2

Related Questions