This React application runs a counter that increments a count variable every two seconds, and updates the display to the browser. The user can start and stop the counter with buttons for each, respectively. It works exactly as expected without the setCount
function, but when this function is enabled, the counter cannot be stopped by either a flag to halt the timer (killswitch) or the clearInterval
. The user needs to be able to see the count, so updating state is obviously necessary, and can’t be omitted.
Why does the timer control become disconnected from the program? My semi-educated noob guess is that it is disconnecting something during the rendering of state.
Is there a way to fix this?
import { useState } from 'react'
export default function Counter() {
let timer = null
let killswitch = false
const [currentCount, setCount] = useState(0)
const start = () => {
if ( killswitch ) return
setCount( prev => prev + 1 )
timer = setTimeout(start, 2000)
}
const stop = () => {
clearTimeout(timer)
killswitch = true
}
return (
<div>
<p>{currentCount}</p>
<p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</p>
</div>
)
} // Counter()
Using setInterval
is not an option for this program, because it is correcting drift between iterations which can’t be done in the interval loop.
This program was also tried with useState for both the killswitch and timer variables. That had no impact on the issue, so I chose the standard javascript variable form for this example, to reduce clutter in the code.
2
Answers
Every time your component renders, you create a brand new
timer
variable. This is a local variable that’s only visible to the specific render in which is was created. If you started the timer on a previous render, you can only cancel it on that same render, not on a later one.Instead, you need a variable that persists from one render to the next. The two options for this are state or a ref, and since the value needed for rendering i’d recommend a ref:
When you perform a setState action the component is re-rendered, at this moment a new timeout is executed but the old timeout is running yet in memory and this is the problem with your component you can stop the recently rendered timeout but old timeout references are deleted.
But don’t worry the best way to handle timmers in react is performing a cleanup in a useEffect hook
const [start, setStart] = useState(false)
The effect should be executed only when the start state changes and the return in the effect is the cleanup function that clear the interval when the component is unmounted, fixed the timmer you must look for other ways to implement the killSwitch
Learn how to handle timmers clearly explained here