5

i'm trying to do countdown timer with react. It will be basically countdown from 10 to 0 and when 0 i will call some function.

i found ideally for me some example: https://codesandbox.io/s/0q453m77nw?from-embed but it's a class component i wan't to do that with functional component and hooks but i can't.

i tried:

function App() {
  const [seconds, setSeconds] = useState(10);
  useEffect(() => {
    setSeconds(setInterval(seconds, 1000));
  }, []);

  useEffect(() => {
    tick();
  });

  function tick() {
    if (seconds > 0) {
      setSeconds(seconds - 1)
    } else {
      clearInterval(seconds);
    }
  }

  return (

    <div className="App">
      <div
        {seconds}
      </div>
    </div>
  );
}

export default App;

it's count down from 10 to 0 very quickly not in 10 seconds. where i mistake ?

Brett DeWoody
  • 59,771
  • 29
  • 135
  • 184
user3348410
  • 2,733
  • 10
  • 51
  • 85
  • Why use a hook? This is basically just as much code as a regular component. – Mike 'Pomax' Kamermans Dec 12 '19 at 21:05
  • 1
    @Mike'Pomax'Kamermans While it's about the same amount of code (at least in the current setup), there are other reasons to prefer a functional component over a class component. Functional components are often easier to read, easier to test, and the React team claims there might be performance incentives in future versions of React. – Brett DeWoody Dec 12 '19 at 21:30

4 Answers4

16

It appears the multiple useEffect hooks are causing the countdown to run more than once per second.

Here's a simplified solution, where we check the seconds in the useEffect hook and either:

  • Use setTimeout to update seconds after 1 second, or
  • Do something else (the function you want to call at the end of the countdown)

There are some downsides to this method, see below.

function App() {
  const [seconds, setSeconds] = React.useState(10);

  React.useEffect(() => {
    if (seconds > 0) {
      setTimeout(() => setSeconds(seconds - 1), 1000);
    } else {
      setSeconds('BOOOOM!');
    }
  });

  return (
    <div className="App">
      <div>
        {seconds}
      </div>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script>

<div id="root"></div>

Downsides

Using setInterval has the downside that it could be stopped - for example, the component is unmounted, you navigate to a different tab, or close your computer. If the timer requires more robustness, the better alternative would be to store an endTime in the state (like a global store or context) and have your component check the current time against the endTime to calculate the countdown.

Brett DeWoody
  • 59,771
  • 29
  • 135
  • 184
3

Do you care about precision? If so, you don't want setInterval. If you don't care about precision (and you probably don't) then you can schedule a call to tick() on an interval, not the other way around.

const TimeoutComponent extends Component {
  constructor(props) {
    super(props);
    this.state = { countdown: 10 };
    this.timer = setInterval(() => this.tick(), props.timeout || 10000);
  }

  tick() {
    const current = this.state.countdown;
    if (current === 0) {
      this.transition();
    } else {
      this.setState({ countdown: current - 1 }); 
    } 
  }

  transition() {
    clearInterval(this.timer);
    // do something else here, presumably.
  }

  render() {
    return <div className="timer">{this.state.countDown}</div>;
  }
}
Mike 'Pomax' Kamermans
  • 49,297
  • 16
  • 112
  • 153
0

This depends on your logic a little bit. In the current situation your useEffect where you run your tick method is running on every render. You can find a naive example below.

function App() {
  const [seconds, setSeconds] = useState(10);
  const [done, setDone] = useState(false);
  const foo = useRef();

  useEffect(() => {
    function tick() {
        setSeconds(prevSeconds => prevSeconds - 1)
    }
    foo.current = setInterval(() => tick(), 1000)
  }, []);

  useEffect(() => {
    if (seconds  === 0) {
      clearInterval(foo.current);
      setDone(true);
    }
  }, [seconds])

  return (

    <div className="App">
    {seconds}
    {done && <p>Count down is done.</p>}
    </div>
  );
}

In the first effect we are doing the countdown. Using callback one for setting state since interval creates a closure. In the second effect we are checking our condition.

devserkan
  • 16,870
  • 4
  • 31
  • 47
0

Simply use this snippet, As it will also help to memoize the timeout callback.

const [timer, setTimer] = useState(60);    
const timeOutCallback = useCallback(() => setTimer(currTimer => currTimer - 1), []);

useEffect(() => {
  timer > 0 && setTimeout(timeOutCallback, 1000);
}, [timer, timeOutCallback]);

console.log(timer);

Hope this will help you or somebody else.

Happy Coding!

Aman Kumar Gupta
  • 2,640
  • 20
  • 18