Search code examples
javascriptreactjstimersetinterval

React & State to create a timer


I am making a timer using React & State only, there is Session time which is set to 2 minutes and Break time, set to 1 minute. When play button is clicked, the timer will start decrementing the value, and when the session time become 0, it will move to break time and when break time become 0, it will move to session time untill the stop button or reset is pressed.

Here I am facing two problems:

  1. At first cycle when session time become 0, it moves to break time. but since break time is set to 1 minutes, its not starting from set time. After first cycle its working as expected.
  2. When I click Play button repeatedly or twice, then I am not able to pause the timer.

Please find my complete code from CodePen

  handleTimer = () => {
      this.interval = setInterval(()=>{
        
        if(this.state.sessionMin == 0 && this.state.Sec == 0){
          clearInterval(this.interval);
          this.setState({
            sessionMin: this.state.sessionConst
          })
       
          this.breakInterval = setInterval(()=>{
            if(this.state.breakMin == 0 && this.state.Sec == 0){
              clearInterval(this.breakInterval);
              this.setState({
                breakMin: this.state.breakConst
                
              })
              console.log(this.state.breakMin)
              this.handleTimer()
            }
            
        
        this.setState({
        display: 'Break',
        Sec: this.state.Sec == 0 ? 10 : this.state.Sec-1,
        breakMin: this.state.Sec == 0 ? this.state.breakMin-1 : this.state.breakMin
      })
            
          },1000)
        }
        
        this.setState({
        display: 'Session',
        Sec: this.state.Sec == 0 ? 10 : this.state.Sec-1,
        sessionMin: this.state.Sec == 0 ? this.state.sessionMin-1 : this.state.sessionMin
      })
      
    },1000)
  }
  

Note: second is set to 10s, instead of 60s for quick test. Note: Reset button is not functioning yet. Note: when ever play button is pressed after stop button, it should continue from that state, not to start over again.

I use 'state' to get latest or most recent data, but it did not work. I don't want to try hooks or any other stuff, want to use only simple component level state.


Solution

  • If you want to skip explanation and go to solution, scroll down.

    1. Let's say, your session timer reached the state Session: 0min 0sec. Look at the code below (with comments):

       // current state: *Session: 0min 0sec*
       this.interval = setInterval(()=>{
         if (this.state.sessionMin == 0 && this.state.Sec == 0) {
         // You tell a browser API "Stop calling this interval function in 
         // future", but execution of current function call continues !!!
         clearInterval(this.interval)
      
         // Execution continues, right? So, this.setState.sessionMin 
         // takes value of this.state.sessionConst, that is 2
         this.setState({ sessionMin: this.state.sessionConst })
      
         // Here you are setting breakInterval, but runtime doesn't care
         // about it for now, because first call of this.breakInterval 
         // will be made after 1000ms
         // (setInterval(() => {}, 1000) 
         // ...wait 1000ms ... 
         // first call 
         // ... wait 1000ns 
         // second call
         // ... etc 
         this.breakInterval = setInterval(()=>{
           // ...breakInterval logic
         },1000)
       }
      
       this.setState({
         display: 'Session',
         // this.state.Sec == 0, so it's value is set to 10
         Sec: this.state.Sec == 0 ? 10 : this.state.Sec-1,
         // As mentioned above, this.state.sessionMin is 2,
         // so here this.state.sessionMin is set to 1 (2 - 1)
         sessionMin: this.state.Sec == 0 ? this.state.sessionMin-1 : this.state.sessionMin
       })
      },1000)
      

    So after execution of code above state of your timer is Session: 1min 10sec, not Break: 0min 10sec as you expected. Then after 1s breakInterval is called, look at the code below:

        // current state: *Session: 1min 10sec*
        this.breakInterval = setInterval(()=>{
          // No, condition is not satisfied, because current 
          // state is *Session: 1min 10sec*
          if(this.state.breakMin == 0 && this.state.Sec == 0){
            // ...clear breakInterval logic 
          }
    
          this.setState({
             // this.state.display will be 'Break'
            display: 'Break',
            // and this.state.Sec will be 9
            Sec: this.state.Sec == 0 ? 10 : this.state.Sec-1,
            // As mentioned above, current state is still **Session: 1min 
            // 10sec**, i.e. this.state.Sec is 10. So in 
            // ternary operator SECOND expression will be executed
            // breakMin: this.state.breakMin
            breakMin: this.state.Sec == 0 ? this.state.breakMin-1 : this.state.breakMin
          })
    
        },1000)
    

    breakMin is initialized in constructor as 1

      this.state={
          display: 'Session',
          sessionMin: 2,
          sessionConst:2,
          // Yep, here
          breakMin: 1,
          breakConst: 1,
          Sec: 0,
        }
    

    Voila! After Session: 1min 10sec, state of your timer is Break: 1min 9sec now. There are several problems here:

    • After session interval this.state.display value is still Session
    • The problem you voiced: incorrect number of minutes when breakInterval starts

    Why after first cycle timer state Session: 1min 10sec changes to Break: 0min 9sec, i.e. number of minutes is correct? Frankly speaking, is not correct. So, after Break: 1min 9sec timer reached Break: 0min 0sec. Again, look at code below:

        // current state: Break: 0min 0sec**
        this.breakInterval = setInterval(()=>{
          // Yes, condition is satisfied
          if(this.state.breakMin == 0 && this.state.Sec == 0){
            // You tell a browser API "Stop calling this function in 
            // future", but current function execution continues !!!
            clearInterval(this.breakInterval)
            // this.state.breakMin is now 1
            this.setState({ breakMin: this.state.breakConst })
            console.log(this.state.breakMin)
            // this.handleTimer() is called, but this.interval()
            // will be called after 1000ms
            this.handleTimer()
          }
    
          this.setState({
            // this.state.display will be 'Break'
            display: 'Break',
            // this.state.Sec will be 10
            Sec: this.state.Sec == 0 ? 10 : this.state.Sec-1,
            // this.state.Sec is still 0, so in ternary operator 
            // FIRST expression will be executed
            // breakMin: this.state.breakMin - 1
            // Because current value of this.state.breakMin is 1,
            // result value will be 0 (1 - 1) !!!!!!!
            breakMin: this.state.Sec == 0 ? this.state.breakMin-1 : this.state.breakMin
          })
    
        },1000)
    

    After execution of code above, actual value of this.state.breakMin will be 0, not 1, and current state will be Break: 0min 10sec. After that, session interval will be called, state will be Session: 1min 9sec, the cycle will start again. Let's say session interval reached Session: 0min 0sec. As you remeber, after that timer state will be Session: 1min 10sec (see the first code example). So:

        // **Session: 1min 10sec**
        this.breakInterval = setInterval(()=>{
          // No, condition is not satisfied
          if(this.state.breakMin == 0 && this.state.Sec == 0){
            // ...clear breakInterval logic 
          }
    
          this.setState({
            display: 'Break',
            // this.state.Sec will be 9
            Sec: this.state.Sec == 0 ? 10 : this.state.Sec-1,
            // this.state.Sec is 10, so in 
            // ternary operator SECOND expression will be executed
            // breakMin: this.state.breakMin
            // Beacause actual value of this.state.breakMin is 0,
            // new breakMin value will be zero too
            breakMin: this.state.Sec == 0 ? this.state.breakMin-1 : this.state.breakMin
          })
    
        },1000)
    

    Therefore, after the first cycle the number of minutes is correct (although everything inside does not work quite as it should)

    1. When you click Play button after first click, you call this.handleTimer method and create a second timer. Then, when you click Stop button, you clear second timer, but the first timer is still working! And you don't have possibility to stop first timer, because this.interval variable was overwritten by a new timer reference.

    So, the solution:

    class TypesOfFood extends React.Component {
      constructor(props) {
        super(props)
        this.state={
          display: 'Session',
          paused: false,
          sec: 0,
          min: this.sessionMinutesConst,
        }
      }
      
      // use 9, not 10 for tests or 59, not 60 in "production"
      // it will be look like:
      // 2:01 -> 2:00 -> 1:59
      secondsInOneMinute = 9
      sessionMinutesConst = 2
      breakMinutesConst = 1
      
      timer = null
      
      decrementTimer = () => {
         this.setState(currentState => ({
            sec: currentState.sec === 0 ? this.secondsInOneMinute : currentState.sec - 1,
            min: currentState.sec === 0 ? currentState.min - 1 : currentState.min
          }))
      }
      
      createTimer = () => { 
        this.timer = setInterval(() => {
          this.handleTimer()
          this.decrementTimer()
        }, 1000)
      }
      
      handleTimer = () => {    
        const { sec, min } = this.state
        const isSessionTimer = this.state.display === 'Session'
        
        if (sec === 0 && min === 0) {
           this.setState({
             display: isSessionTimer ? 'Break' : 'Session',
             min: isSessionTimer ? this.breakMinutesConst : this.sessionMinutesConst
           })
        }
      }
    
      handleStop = () => {
        this.setState({ paused: true })
        clearInterval(this.timer)
      }
      
      handleReset = () => {
        clearInterval(this.timer)
        this.timer = null
        
        this.setState({
          display: 'Session',
          paused: false,
          sec: 0,
          min: this.sessionMinutesConst,
        })
      }
      
      handlePlay = () => {
        if (this.timer && !this.state.paused) return
        
        this.createTimer()
        this.setState({ paused: false })
      }
    
      render() {
        return (
          <div id="container">
            <h3>{this.state.display}</h3>
            <h1>{this.state.min}m:{this.state.sec}s</h1>
            <button onClick={this.handlePlay}>Play</button>
            <button onClick={this.handleStop}>Stop</button>
            <button onClick={this.handleReset}>Reset</button>
          </div>
        )
      }
    }
    

    What you should pay attention to:

    • I replaced two timers with only one timer. When timer state is [current timer]: 0min 0sec, this.state.display and this.state.min changes to new values - Session or Break and this.sessionMinutesConst or this.breakMinutesConst respectively in handleTimer method.
    • timer property in class. When timer is created, it's reference is asigned to this property, so it is easy to clear timer whenever we want
    • handleStop method. I added paused property to the state, so it's easy to understand, when timer is stopped and when it is fully cleared
    • handlePlay method. New timer is created only when there is no current timer or when there is current timer, but it is just paused.
    • I removed your binds in constructor - it's not necessary when declaring methods as arrow functions
    • I implemented handleReset method

    Hope that helped!