Timer

Develop a timer where users can start, stop, and reset. Display the time in a digital format.

Solution

0

Explanation

Overview

setInterval is the basis of how you create a timer. But the trick comes from knowing how to interact with it and to clean it up correctly to avoid adverse behavior.

useRef rather than useState

To keep track of the timer we need to store it in a useRef. Now we are confident that when the page rerenders we don't lose access to this reference. This is an appropriate use case for storing data in a reference that you can look up than with constant monitoring that you get with state.

User interaction

For the user to interact with the timer you need 3 buttons and 3 functions: handleStart, handleStop and handleReset. The functions for each of these are very simple to update the status of the timer.

How to use useEffect

However, logically you will handle the timer with the use of status and useEffect. Here's the breakdown of the useEffect code:

The key to implementation is in the dependency array in the useEffect function. If status is updated, the component will be rerendered.

A status of true, tells us that the timer should be running. The if statement checks if this is true and creates a setInterval. If status is false, meaning the timer should not be running, then we need to remove the setInterval reference. This is why it was key to have this stored in a reference.

Keep it clean with useEffect cleanup function

Note that in useEffect there is a return statement. This is standard practice to cleanup functions that may be running when the component is unmounted. Otherwise this could lead to memory overload.

Code

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

const Solution = () => {
  const [time, setTime] = useState(0);
  const [status, setStatus] = useState(false);

  const timerRef = useRef<NodeJS.Timeout | null>(null);

  const timingFunction = () => {
    setTime((prevTime) => prevTime + 1);
  };

  useEffect(() => {
    if (status) {
      timerRef.current = setInterval(timingFunction, 1000);
    } else {
      if (timerRef.current) clearInterval(timerRef.current);
    }

    return () => {
      if (timerRef.current) clearInterval(timerRef.current);
    };
  }, [status]);

  const handleStart = () => {
    setStatus(true);
  };

  const handleStop = () => {
    setStatus(false);
  };

  const handleReset = () => {
    setStatus(false);
    setTime(0);
  };

  return (
    <div>
      <div>{time}</div>
      <div className={styles.controls}>
        <button
          onClick={handleStart}
          className={`${styles.button} ${styles.start}`}
        >
          Start
        </button>
        <button
          onClick={handleStop}
          className={`${styles.button} ${styles.stop}`}
        >
          Stop
        </button>
        <button
          onClick={handleReset}
          className={`${styles.button} ${styles.reset}`}
        >
          Reset
        </button>
      </div>
    </div>
  );
};

export default Solution;

Styling

.controls {
    display: flex;
    gap: 10px;
    margin: 10px 0;
  }
  
  .button {
    padding: 5px 30px;
    border: 1px solid grey;
    border-radius: 5px;
    cursor: pointer;
    transition: filter 0.3s;
  }
  
  .button:hover,
  .button:active {
    filter: brightness(0.8);
  }
  
  .start {
    background-color: hsl(120, 41%, 68%);
  }
  
  .stop {
    background-color: hsl(12, 41%, 68%);
  }
  
  .reset {
    background-color: hsl(60, 56%, 55%);
  }
  

Links