Countdown Timer

Create a countdown timer where users can set a duration, and it counts down to zero with updates displayed visually.

Solution

0

Explanation

The HTML

Structurally what you see is what you get. An input, button and div for the display.

Starting the timer

useRef is an efficient way to get the value from the input.

The handleStart function, will parse the input as a number, then we have a count state to track the value.

Mimicking a timer

Now we can set the count value, we need to make the timer count down in seconds.

The useEffect function allows us to update the counter when the count state gets updated.

The intervalRef variable keeps a setInterval reference to allow us to update and clear. The if statements contain a lot of checks that the value is valid and that there are no old values still around.

The fundamental element to make this function is the setCount function that takes the current count and reduces it by 1.

Disclaimer: using setInterval is not an accurate way to measure time but works well in simple cases.

Cleanup

You'll notice the the many areas where we check the status of the setInterval ref. We mentioned the check to see if a ref exists it needs to be cleared first. But there is also a return statement in the useEffect that is called a cleanup function. This makes sure that when we leave this page and the component unmounts, if there is still an interval present, it is cleared.

Code

import { useRef, useState, useEffect } from "react";
import styles from "./Solution.module.css";

const Solution = () => {
  const inputRef = useRef<null | HTMLInputElement>(null);
  const intervalRef = useRef<number | null>(null);
  const [count, setCount] = useState(0);

  const handleStart = () => {
    const inputValue = inputRef.current?.value;
    const numberValue = inputValue ? parseInt(inputValue, 10) : 0;
    setCount(numberValue); // Set count and start/restart timer
  };

  // this was required for multiple start presses
  useEffect(() => {
    // Only start a new timer if count is greater than 0
    if (count > 0) {
      if (intervalRef.current !== null) {
        window.clearInterval(intervalRef.current); // Clear existing timer
      }
      intervalRef.current = setInterval(() => {
        setCount((prevCount) => (prevCount > 0 ? prevCount - 1 : 0));
      }, 1000) as unknown as number;
    } else if (intervalRef.current) {
      window.clearInterval(intervalRef.current);
      intervalRef.current = null;
    }

    return () => {
      // Cleanup interval on component unmount or count changes
      if (intervalRef.current !== null) {
        window.clearInterval(intervalRef.current);
      }
    };
  }, [count]); // Effect depends on count

  return (
    <>
      <label className={styles.label} htmlFor="time">
        Set time in seconds
      </label>
      <input
        ref={inputRef}
        type="number"
        id="time"
        className={styles.timeInput}
        defaultValue={0}
      />
      <button onClick={handleStart} type="button" className={styles.button}>
        Start
      </button>
      <div className={styles.display}>{count}</div>
    </>
  );
};

export default Solution;

Styling

.timeInput,
.button,
.display {
  padding: 10px;
  margin: 5px 0;
}

.label {
  display: block;
}

.timeInput {
  display: inline-block;
  width: 60px;
  margin-right: 5px;
}

.button {
  display: inline;
  cursor: pointer;
}

.display {
  display: block;
  width: 60px;
  border: 1px solid gray;
}

Links