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>
        
      );
    }
  }
2
  • 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. Commented Jul 23, 2020 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 offers a higher resolution. See setState does not update state immediately inside setInterval for an approach that uses RAF and Date. Commented Jun 27, 2021 at 1:00

2 Answers 2

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!

Sign up to request clarification or add additional context in comments.

2 Comments

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?
Exactly that! You got it ;)
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 :)

5 Comments

Thanks John! May I ask about the use of 'options'? Could we just have done this.interval = setInterval(this.tick, 1000) ?
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
@ari for example you can say run 4 times a second and count down from 50 jumping by 10 each time. <Timer start={50} interval={250} step={10} />
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 <Timer myTimer/> inside another component but I can't seem to call let done = this.props.myTimer.isDone() inside another component as it throws an error
just pass an onComplete callback to the timer as a prop. so that when the counting down finishes you can call it

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.