How to Create a Simple React Countdown Timer
Tags: react, javascript, typescript, tutorial
Published: 2020-01-07 05:29:28

Visit my Blog for the original post: [How to Create a Simple React Countdown Timer](https://yizhiyue.me/2019/12/08/how-to-create-a-simple-react-countdown-timer/)

# A Few Words in Front

Today I am going to share one interesting and useful small front-end feature implementation in React, a simple count down timer.

# Solution

The correct implementation can be found at [simple-react-countdown-timer](https://codesandbox.io/s/simple-react-countdown-timer-zyfr0) if you wish to implement quickly without reading through my explanation.

```tsx
import * as React from "react";
import { render } from "react-dom";

import "./styles.css";

function App() {
  const [counter, setCounter] = React.useState(60);

  // First Attempts
  // setInterval(() => setCounter(counter - 1), 1000);

  // Second Attempts
  // React.useEffect(() => {
  //   counter > 0 && setInterval(() => setCounter(counter - 1), 1000);
  // }, []);

  // Second Attempts - Inspection
  // React.useEffect(() => {
  //   counter > 0 &&
  //     setInterval(() => {
  //       console.log(counter);
  //       setCounter(counter - 1);
  //     }, 1000);
  // }, []);

  // Third Attempts
  // React.useEffect(() => {
  //   const timer =
  //     counter > 0 && setInterval(() => setCounter(counter - 1), 1000);
  //   return () => clearInterval(timer);
  // }, [counter]);

  // Suggested by Laurent
  React.useEffect(() => {
    counter > 0 && setTimeout(() => setCounter(counter - 1), 1000);
  }, [counter]);

  return (
    <div className="App">
      <div>Countdown: {counter}</div>
    </div>
  );
}

const rootElement = document.getElementById("root");
render(<App />, rootElement);

```

# Explanation

## First attempt, in an intuitive way

Initially, we utilise `useState` react hook to create a new state variable `counter` in the functional component. `counter` holds the number of seconds the counter should start with. Then a native JavaScript function, `setInterval` is called to trigger `setCounter(counter - 1)` for every 1000ms. Intuitively, it represents the number decreases by 1 every 1 second.

```tsx
function App() {
  const [counter, setCounter] = React.useState(60);

  // First Attempts
  setInterval(() => setCounter(counter - 1), 1000);

  return (
    <div className="App">
      <div>Countdown: {counter}</div>
    </div>
  );
}
```

However, it works, in a terrible way. You can clearly notice that Initially the countdown works fine but then start to gradually accelerate.

![CountDown 1](https://imgur.com/Lsx9AHt.gif)

That is because every time when `setCounter` is triggered, the `App` component get re-rendered. As the component is re-rendered, the `App()` function is executed again, therefore, the `setInterval()` function triggers again. Then there are 2 `setInterval()` running at the same time and both triggering `setCounter()`, which again, creates more `setInterval()`.

Therefore, more and more `setInterval()` are created and the counter is deducted for more and more times, finally resulting in accelerating decrement.

## Second attempt, utilizing useEffect hook

Ok, maybe we can solve the problem by just trigger the `setInterval()` once in the life cycle of a component by using `useEffect()` react hook.

```tsx
function App() {
  const [counter, setCounter] = React.useState(60);

  // Second Attempts
  React.useEffect(() => {
    counter > 0 && setInterval(() => setCounter(counter - 1), 1000);
  }, []);

  return (
    <div className="App">
      <div>Countdown: {counter}</div>
    </div>
  );
}
```

`useEffect` is a react hook which accepts parameters including a function to be triggered at a specific point of time and an array of dependencies.

- If the dependencies are not specified, the function is triggered every time any state inside of this component is updated.
- If the dependencies are specified, only when the particular dependant state is changed, the function is triggered.
- If the dependency array is empty, then the function is only triggered once when the component is initially rendered.

So in this way, surely `setInterval()` can only be triggered once when the component is initially rendered.

Are we getting the correct result here?

![CountDown 2](https://i.imgur.com/PxGOviI.gif)

Wrong again! The countdown mysteriously freezes after being decremented by 1. I thought `setInterval()` should be running continuously? Why it is stopped? To find out what happened, let's add a `console.log()`.

```tsx
React.useEffect(() => {
  counter > 0 &&
    setInterval(() => {
      console.log(counter);
      setCounter(counter - 1);
    }, 1000);
}, []);
```

Now the console prints out:

![Countdown 3](https://imgur.com/000KWpi.gif)

All the numbers printed out are 60, which means the counter itself has not been decreased at all. But `setCounter()` definitely has run, then why isn't the `counter` updated?

This `counter` is indeed not decreased because the `setCounter` hook essentially does not change the `counter` within **THIS** function. The following illustration may make things clearer.

![Countdown 4](https://imgur.com/4Qa8KFE.png)

Because every time when the component is re-rendered, the `App()` function is called again. Therefore, within the `App()` scope, only in the first time, the `useEffect()` is triggered and the `setInterval()` is within the first time `App()` scope with the property `counter` always equal to 60.

In the global environment, there is only one `setInterval()` instance which contiguously set the `counter` to 59, causing new `App()` calls always get the state `counter` to be 59. That's why the counter seems to be freezed at 59. But in fact, it is not freezed, it is being reset all the time but the value is **ALWAYS** 59.

## Third Attempts, useEffect with cancelling interval

To overcome the issue mentioned above, we need to trigger the `setInterval()` in every single `App()` call with different `counter` value, just as illustrated below.

![Countdown 5](https://imgur.com/U4CgZPw.png)

To achieve that, we need to do 2 things:

1. Let `setInterval()` get triggered every time when component gets re-rendered
   Solution: add a dependency of `counter` in `useEffect` hook so that every time when the `counter` changes, a new `setInterval()` is called.
2. Clear `setInterval()` in this scope to avoid duplicated countdown
   Solution: add a callback function in `useEffect` hook to clear the interval in current scope so that only one `setInterval()` instance is running in the global environment at the same time.

Thus, the final solution is

```tsx
function App() {
  const [counter, setCounter] = React.useState(60);

  // Third Attempts
  React.useEffect(() => {
    const timer =
      counter > 0 && setInterval(() => setCounter(counter - 1), 1000);
    return () => clearInterval(timer);
  }, [counter]);

  return (
    <div className="App">
      <div>Countdown: {counter}</div>
    </div>
  );
}
```

And it looks correct!

![Countdown 6](https://imgur.com/lKjNgu9.gif)

Thank you for reading!!


# Update on 9 Dec 2019

Thanks to [@Laurent](https://www.linkedin.com/in/laurentsteff/), he suggested me to use `setTimeout()` to replace `setInterval()` in the final solution, which I think it's a better idea! `setTimeout()` only runs once, hence, we don't have to clear the `setInterval()` in every `useEffect()` change. Wonderful!