Timer
Develop a timer where users can start, stop, and reset. Display the time in a digital format.
Solution
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%);
}