Search code examples
javascriptreactjsrenderingsetintervaluse-ref

How setInterval works in the background of React Re-Rendering


I'm creating a simple countdown timer app with React and I am having hard time understanding how setInterval works while React re-renders the component. For example, this following code had timer continue to run even though I had used clearInterval onPause().

    let startTimer;

    const onStart = () => {
        startTimer = setInterval( ()=>{
            if ( timeRemaining === 0 ) {
              clearInterval(startTimer);
              setIsCounting(false)
              return 
            }
            updateTimer()
          }, 1000)                        
          setIsCounting( (prev) => !prev )
     } // end of onStart

    const onPause = () => {
        setIsCounting( (prev) => !prev )
        clearInterval(startTimer)
    }

    return (
             { props.isCounting ? 
               <button onClick={props.onPause}> Pause </button> 
             : <button onClick={props.onStart}> Start </button> }
    )

However, the timer successfully pauses when I simply change

let starter; 

to

let startTimer = useRef(null)

const onStart = () => {
        startTimer.current = setInterval( ()=>{
            if ( timeRemaining === 0 ) {
              clearInterval(startTimer);
              setIsCounting(false)
              return 
            }
            updateTimer()
          }, 1000)                        
          setIsCounting( (prev) => !prev )
     } // end of onStart

    const onPause = () => {
        setIsCounting( (prev) => !prev )
        clearInterval(startTimer.current)
    }

What's happening to setInterval when React re-renders its component? Why did my timer continue to run when I didn't use useRef()?


Solution

  • A ref provides what's essentially an instance variable over the lifetime of a component. Without that, all that you have inside an asynchronous React function is references to variables as they were at a certain render. They're not persistent over different renders, unless explicitly done through the call of a state setter or through the assignment to a ref, or something like that.

    Doing

    let startTimer;
    
    const onStart = () => {
        startTimer = setInterval( ()=>{
    

    could only even possibly work if the code that eventually calls clearInterval is created at the same render that this setInterval is created.

    If you create a variable local to a given render:

    let startTimer;
    

    and then call a state setter, causing a re-render:

    setIsCounting( (prev) => !prev )
    

    Then, due to the re-render, the whole component's function will run again, resulting in the let startTimer; line running again - so it'll have a value of undefined then (and not the value to which it was reassigned on the previous render).

    So, you need a ref or state to make sure a value persists through multiple renders. No matter the problem, reassigning a variable declared at the top level of a component is almost never the right choice in React.