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>
}
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.
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>
}
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>
}
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>
}
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>
}