Anzo
Anzo

Reputation: 3

Weird issue with setState and Callback

I'm coding a simple pomodoro timer which is fired using a click of the button.

 tick() {
    if (this.state.minutes > 0) {
      setInterval(() => {
        this.timer(this.state.minutes * 60 + this.state.seconds - 1);
      }, 1000);
    }
  }
  timer(x) {
    this.setState(
      {
        minutes: Math.trunc(x),
        seconds: (x*60) % 60
      },
      () => this.tick()
    );
  }

  render() {
    return (
      <div className="App">
        <div className="Pomodo">
          <div className="counter">
            {this.state.minutes + ":" + this.state.seconds}
          </div>
          <button className="btnCode" onClick={() => this.timer(25)}>
            Code
          </button>
          <button className="btnCoffee" onClick={() => this.timer(5)}>
            Coffee
          </button>
          </div>
      </div>
    );
  }
}

export default App;

This shows the timer as follow:

25:00 (correct)

24:59 (correct)

24:57 == wrong should be 58

24:53 == wrong should be 57

...etc

What am I missing here please? When troubleshooting via chrome the counter is fine and shows the correct numbers.

Upvotes: 0

Views: 41

Answers (1)

Sai Sandeep Vaddi
Sai Sandeep Vaddi

Reputation: 290

It is skipping the seconds because, everytime you call tick(), you are setting a new interval by calling this.tick() in setState. You can fix this by adding a flag after calling tick for the first time.

One more issue I see is, in the btnCode's onClick, you are passing 25 as minutes to this.timer. But, in setInterval, you are calling timer with everything seconds. I suggest you pass everything in seconds to timer function. Also, it is good to clear intervals at unmounting.

See the adjusted code.

class TodoApp extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
    	minutes: 0,
      seconds: 0,
      started: false
    }
    this.tick = this.tick.bind(this);
    this.timer = this.timer.bind(this);
  }
  
   tick() {
   // Add flag so that tick is not called again
   this.setState({
    started: true
   })
   
    if (this.state.minutes > 0) {
      this.interval = setInterval(() => {
       const seconds = this.state.minutes * 60 + this.state.seconds - 1;
        this.timer(seconds);
      }, 1000);
    }
  }
  componentWillUnmount() {
    clearInterval(this.interval);
  }
  timer(x) {
    this.setState(
      {
        minutes: Math.trunc(x / 60),
        seconds: x % 60
      },
      () => !this.state.started && this.tick() // only call if its timer is not started before
    );
  }

  render() {
    return (
      <div className="App">
        <div className="Pomodo">
          <div className="counter">
            {this.state.minutes + ":" + this.state.seconds}
          </div>
          <button className="btnCode" onClick={() => this.timer(25 * 60)}>
            Code
          </button>
          <button className="btnCoffee" onClick={() => this.timer(5 * 60)}>
            Coffee
          </button>
          </div>
      </div>
    );
  }

}

ReactDOM.render(<TodoApp />, document.querySelector("#app"))
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

Upvotes: 1

Related Questions