setTimeout for this.state vs useState

Updated Version:

Question: Difference in behavior of a React State variable inside setTimeout / setInterval for function and class components?

Case 1: State variable in function component (stale closure):

const [value, setValue] = useState(0)

useEffect(() => {
  const id = setInterval(() => {
    // It will always print 0 even after we have changed the state (value)
    // Reason: setInterval will create a closure with initial value i.e. 0
    console.log(value)
  }, 1000)
  return () => {
    clearInterval(id)
  }
}, [])

Case 2: State variable in class component (no stale closure):

constructor(props) {
  super(props)
  this.state = {
    value: 0,
  }
}

componentDidMount() {
  this.id = setInterval(() => {
    // It will always print current value from state
    // Reason: setInterval will not create closure around "this"
    // as "this" is a special object (refernce to instance)
    console.log(this.state.value)
  }, 1000)
}

Case 3: Let’s try to create a stale closure around this

// Attempt 1

componentDidMount() {
  const that = this // create a local variable so that setInterval can create closure
  this.id = setInterval(() => {
    console.log(that.state.value)
    // This, too, always print current value from state
    // Reason: setInterval could not create closure around "that"
    // Conclusion: Oh! that is just a reference to this (attempt failed)
  }, 1000)
}

Case 4: Let’s again try to create a stale closure in class component

// Attempt 2

componentDidMount() {
  const that = { ...this } // create a local variable so that setInterval can create closure
  this.id = setInterval(() => {
    console.log(that.state.value)
    // Great! This always prints 0 i.e. the initial value from state
    // Reason: setInterval could create closure around "that"
    // Conclusion: It did it because that no longer is a reference to this,
    // it is just a new local variable which setInterval can close around
    // (attempt successful)
  }, 1000)
}

Case 5: Let’s again try to create a stale closure in class component

// Attempt 3

componentDidMount() {
  const { value } = this.state // create a local variable so that setInterval can create closure
  this.id = setInterval(() => {
    console.log(value)
    // Great! This always prints 0 i.e. the initial value from state
    // Reason: setInterval created closure around value
    // Conclusion: It is easy! value is just a local variable so it will be closed
    // (attempt successful)
  }, 1000)
}

Case 6: Class has won (no extra effort to avoid the stale closure). But, how to avoid it in function component?

// Let's find solution

const value = useRef(0)

useEffect(() => {
  const id = setInterval(() => {
    // It will always print the latest ref value
    // Reason: We used ref which gives us something like an instance field.
    // Conclusion: So, using ref is a solution
    console.log(value.current)
  }, 1000)
  return () => {
    clearInterval(id)
  }
}, [])

source-1, source-2

Case 6: Let’s find another solution for function components

useEffect(() => {
  const id = setInterval(() => {
    // It will always print the latest state value
    // Reason: We used updater form of setState (which provides us latest state value)
    // Conclusion: So, using updater form of setState is a solution
    setValue((prevValue) => {
      console.log(prevValue)
      return prevValue
    })
  }, 1000)
  return () => {
    clearInterval(id)
  }
}, [])

Original Version:

The issue is caused by closures and can be fixed by using ref. But here is a workaround to fix it i.e. access the latest state value using “updater” form of setState:

function App() {

  const [count, setCount] = React.useState(0);

  React.useEffect(() => {
    setTimeout(() => console.log('count after 5 secs: ', count, 'Wrong'), 5000)
  }, [])

  React.useEffect(() => {
    setTimeout(() => {
      let count
      setCount(p => { 
        console.log('p: ', p)
        count = p
        return p
       })
      console.log('count after 5 secs: ', count, 'Correct')
    }, 5000);
  }, [])

  return (<div>
    <button onClick={() => setCount(p => p+1)}>Click me before 5 secs</button>
    <div>Latest count: {count}</div>
  </div>)
}

ReactDOM.render(<App />, document.getElementById('mydiv'))
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<body>
<div id="mydiv"></div>
</body>

Leave a Comment