[React] Use setState inside setInterval()

May 21, 2021

programming
article cover

Can’t update state

In React, when you try to update state inside setInterval() like this, it wouldn’t work.

import React, { useEffect, useState } from 'react'

export const Sample = () => {
  const [count, setCount] = useState(0)
  
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('ticking')
      setCount(count + 1)
    }, 1000)
    
    return () => clearInterval(timer)
  }, [])
  
  
  return <div>count is: {count}</div>
}

screen shot

Trying to put it outside of useEffect() doesn’t make any difference.

import React, { useEffect, useState } from 'react'

export const Sample = () => {
  let timer
  const [count, setCount] = useState(0)
  
  const updateCount = () => {
    timer = !timer && setInterval(() => {
      console.log('ticking')
      setCount(count + 1)
    }, 1000)
  }
  
  useEffect(() => {
    updateCount()
    
    return () => clearInterval(timer)
  }, [])
  
  
  return <div>count is: {count}</div>
}

The reason is that the function passed to setInterval() remains the same as when it was passed, so the variables in it also do not change.

This means when we pass the following function to setInterval(),

() => {
  console.log('ticking')
  setCount(count + 1)
}

“count” is always 0 which is the initial value. So updated state can only be 1.

Functional updates

In fact, we can pass a function to setState() that takes the value of the previous state as an argument then return an updated value.

screen shot

This can be used to update the state inside setInterval()

import React, { useEffect, useState } from 'react'

export const Sample = () => {
  let timer
  const [count, setCount] = useState(0)
  
  const updateCount = () => {
    timer = !timer && setInterval(() => {
      console.log('ticking')
+     setCount(prevCount => prevCount + 1) // new
    }, 1000)
  }
  
  useEffect(() => {
    updateCount()
    
    return () => clearInterval(timer)
  }, [])
  
  
  return <div>count is: {count}</div>
}

screen shot

It’s working!

Use a state inside setInterval()

There is still a slight problem.
For example, when count reaches 3, we want to do something and stop updating the state.

We can add an if statement like this, but it won’t stop it.

import React, { useEffect, useState } from 'react'

export const Sample = () => {
  let timer
  const [count, setCount] = useState(0)
  
  const updateCount = () => {
    timer = !timer && setInterval(() => {
      console.log('ticking')
      setCount(prevCount => prevCount + 1)
    }, 1000)
    
+   if (count === 3) {     // new
+     console.log('stop!')
+     clearInterval(timer)
+   }
  }
  
  useEffect(() => {
    updateCount()
    
    return () => clearInterval(timer)
  }, [])
  
  
  return <div>count is: {count}</div>
}

screen shot

The reason here is updateCount() is executed only once. So the if statement we added will be considered only at first time.

If you put it in setCount() as follows, it’s OK, but it’s really unpleasant that the inside of setState gets bigger.

setCount(prevCount => {
  prevCount + 1
  
  if (prevCount+1 === 3) {
    // do something
  }
})

This problem can be solved by adding “count” to the second argument of useEffect().
This will cause useEffect() to be executed every time the value of count is updated, so the count in updateCount() will always have a new value.

import React, { useEffect, useState } from 'react'

export const Sample = () => {
  let timer
  const [count, setCount] = useState(0)
  
  const updateCount = () => {
    timer = !timer && setInterval(() => {
      console.log('ticking')
      setCount(prevCount => prevCount + 1)
    }, 1000)
    
    if (count === 3) {
      console.log('stop!')
      clearInterval(timer)
    }
  }
  
  useEffect(() => {
    updateCount()
    
    return () => clearInterval(timer)
+ }, [count])  // new
  
  
  return <div>count is: {count}</div>
}

screen shot

As you might have noticed, we can now update the state without using functional updates.

setCount(count + 1)

But in this way, useEffect() gets messy every time the variables we want to use increase. So me personally, I prefer to keep using functional updates if I only need to update state.

Final code

import React, { useEffect, useState } from 'react'

export const Sample = () => {
  let timer
  const [count, setCount] = useState(0)
  
  const updateCount = () => {
    timer = !timer && setInterval(() => {
      setCount(prevCount => prevCount + 1)
    }, 1000)
    
    if (count === 3) clearInterval(timer)
  }
  
  useEffect(() => {
    updateCount()
    
    return () => clearInterval(timer)
  }, [count])
  
  
  return <div>count is: {count}</div>
}

Profile picture

Photographer / Web engineer working in Berlin.
A Japanese living with a Slovak wife and two German cats.