skip to Main Content

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


  1. 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:

    export default function Counter() {
    
        const timer = useRef(null);
        const killswitch = useRef(false);
    
        const [currentCount, setCount] = useState(0)
    
        const start = () => {
            if ( killswitch.current ) return
            setCount( prev => prev + 1 )
            timer.current = setTimeout(start, 2000)
        }
    
        const stop = () => {
            clearTimeout(timer.current)
            killswitch.current = true
        }
    
        return (
            <div>
                <p>{currentCount}</p>
                <p>
                    <button onClick={start}>Start</button>
                    <button onClick={stop}>Stop</button>
                </p>
            </div>
        )
    } 
    
    Login or Signup to reply.
  2. 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)

    useEffect(() => {
    let interval;
    if(start) {
      interval = setInterval(() => getTime(deadline), 1000);
      setStart(false)
    }
    //perform a cleanup
    return () => clearInterval(interval);  }, [start]);
    

    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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search