Jasper
Jasper

Reputation: 27

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()?

Upvotes: 1

Views: 289

Answers (1)

CertainPerformance
CertainPerformance

Reputation: 371098

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.

Upvotes: 1

Related Questions