1

Beginner here. I'm trying to make a countdown timer from 3 to 0. The app renders the seconds to the screen, but it does so really quickly. I tried changing the interval but it doesn't seem to make the time anymore accurate. I don't know what is going wrong. Any help is appreciated.

import React from "react";

export default class Timer extends React.Component {

    constructor(){
        super();

        this.state = {
            time: 3,
        }

        this.countdown = this.countdown.bind(this);
        this.timer = this.timer.bind(this)
    }

    timer(){
        
        let interval = setInterval(() => this.countdown(interval),1000)
        
        return this.state.time
    }

    countdown(t){
        if(this.state.time == null)
        {
            console.log("NULL")
        }
        let myTime = this.state.time
        
        if(myTime > 0) {
            myTime--;
            this.setState({time: myTime})
            console.log(myTime)
        } else {
            clearInterval(t)
        }

        return myTime;
    }

    render() {
      return (
        <div id = "Timer">
          <p>
              {this.timer()}
          </p>
   
        </div>
        
      );
    }
  }
ari
  • 17
  • 4
  • You can't start the interval in the same function that prints the value. Every time you print the value, it starts a new interval so you'll have a bunch of them running at the same time. Look up `componentDidMount` in the documentation. – Guy Incognito Jul 23 '20 at 20:15
  • Even though existing answers fix your bugs, they still use `setInterval`, which isn't very accurate without assistance to avoid drift. [`requestAnimationFrame`](https://stackoverflow.com/questions/38709923/why-is-requestanimationframe-better-than-setinterval-or-settimeout) offers a higher resolution. See [setState does not update state immediately inside setInterval](https://stackoverflow.com/questions/62328108/setstate-does-not-update-state-immediately-inside-setinterval/62328634#62328634) for an approach that uses RAF and `Date`. – ggorlen Jun 27 '21 at 01:00

2 Answers2

1

I would use componentDidMount here to start the interval going. You only want to create the interval once and then clean it up when either it finishes counting down or if the component unmounts before the timer has reached 0. You can build extra functionality ontop of this to do things like stop / start again... etc.

export default class Timer extends React.Component {
  state = {
    time: this.props.start || 3
  };
  options = {
    interval: this.props.interval || 1000
    step: this.props.step || 1
  };
  interval = null;

  componentDidMount() {
    this.countdown()
  }
  componentWillUnmount() {
    clearInterval(this.interval)
  }
  tick = () => {
    this.setState(
      ({ time }) => ({ time: time - this.options.step }),
      () => {
        if (this.state.time === 0) {
          clearInterval(this.interval);
        }
      }
    );
  }
  countdown = () => {
    this.interval = setInterval(this.tick, this.options.interval);
  }

  render() {
    return (
      <div id="Timer">
        <p>{this.state.time}</p>
      </div>
    );
  }
}

Here's a demo to play with :)

John Ruddell
  • 25,283
  • 6
  • 57
  • 86
  • Thanks John! May I ask about the use of 'options'? Could we just have done this.interval = setInterval(this.tick, 1000) ? – ari Jul 23 '20 at 20:49
  • yes you could easily do that as well. I just added a more generic options object that you can use to add more features in the future. Letting someone define the frequency to tick at is nice for potential future use cases – John Ruddell Jul 23 '20 at 20:59
  • @ari for example you can say run 4 times a second and count down from 50 jumping by 10 each time. `` – John Ruddell Jul 23 '20 at 21:43
  • this is a little unrelated, but I wanted to ask if there was a way to tell that the timer is done from another component. (Not sure if I should make another post, but I might). I added `done: true` to state once timer hits 0, and a function isDone() which returns `this.state.done`, and inside another component but I can't seem to call `let done = this.props.myTimer.isDone()` inside another component as it throws an error – ari Jul 23 '20 at 21:53
  • just pass an `onComplete` callback to the timer as a prop. so that when the counting down finishes you can call it – John Ruddell Jul 24 '20 at 05:47
1

The user that firs commented your post is right. But let me clarify.

This is what I think that is happening. The first time your component renders execute the timer() method, which set the timer interval. After the first second, the interval callback is executed, which change the component state, and react schedule a re-render of your component. Then, the component re-renders itself, and execute the timer() function again before the 2nd second (please forgive this redundant phrase) which sets a new interval. And this occurs until you clear the interval, which is the last interval your code have set. That is why you notice the value of variable time change oddly fast.

You should do something like this: (this is your very same code with a few changes, may be is more useful for you to understand. Then you can give your own style or personal flavor)

import React from "react";

export default class Timer extends React.Component {

constructor(){
    super();

    this.state = {
        time: 3,
    }

    this.countdown = this.countdown.bind(this);
    this.timer = this.timer.bind(this)
}

componentDidMount() {
  this.interval = setInterval(() => 
    this.countdown(interval),1000
  );
}

componentWillUnmount() {
  if (this.interval) {
     clearInterval(this.interval);
  }
}

countdown(){
    if(this.state.time == null)
    {
        console.log("NULL")
    }
    let myTime = this.state.time
    
    if(myTime > 0) {
        myTime--;
        this.setState({time: myTime})
        console.log(myTime)
    } else {
        clearInterval(this.interval)
    }

    return myTime;
}

render() {
  return (
    <div id = "Timer">
      <p>
          {this.state.time}
      </p>
    </div>
  );
}
}

Cheers!

Bruno Sendras
  • 571
  • 5
  • 7
  • I see. That does clear my understanding. For clarification, the problem was in my timer function, where it created new intervals each time setState() was called/ the app rerendered. Which we then fix by using componentDidMount() so that it only creates the interval once? – ari Jul 23 '20 at 20:58
  • Exactly that! You got it ;) – Bruno Sendras Jul 23 '20 at 21:00