How to develop a Stopwatch in React JS with custom hook
Abdul Basit
Posted on July 19, 2020
In order to truly understand how things work we should break down the task into smaller pieces, this is what we are doing here. Our ultimate goal is to build a React Native Pomodoro clock App but first, we will build a stopwatch to understand how setInterval
and clearInterval
works in react with hooks then turn this stopwatch into a Pomodoro clock, and so on.
Let's start
Let's break down everything and build a boilerplate first.
import React, { useState } from 'react';
import './App.css';
const App = () => {
const [timer, setTimer] = useState(0)
const [isActive, setIsActive] = useState(false)
const [isPaused, setIsPaused] = useState(false)
const countRef = useRef(null)
const handleStart = () => {
// start button logic here
}
const handlePause = () => {
// Pause button logic here
}
const handleResume = () => {
// Resume button logic here
}
const handleReset = () => {
// Reset button logic here
}
return (
<div className="app">
<h3>React Stopwatch</h3>
<div className='stopwatch-card'>
<p>{timer}</p> {/* here we will show timer */}
<div className='buttons'>
<button onClick={handleStart}>Start</button>
<button onClick={handlePause}>Pause</button>
<button onClick={handleResume}>Resume</button>
<button onClick={handleReset}>Reset</button>
</div>
</div>
</div>
);
}
export default App;
A timer will start from 0 to onward by clicking the start button.
isActive
is defined to see if the timer is active or not.
isPaused
is defined to see if the timer is paused or not.
Initially, both values will be false
. We have defined these values to conditionally render Start, Pause, and Resume button.
UseRef hook
useRef
helps us to get or control any element's reference.
It is the same as we get the reference in vanilla javascript by using document.getElementById("demo")
which means we have skipped virtual dom and directly dealing with browsers. Isn't the useRef
hook is powerful?
If we run this code we will see the result like this. (CSS is included at the end of the article)
Now we have three tasks to do,
- to write a function for each button
- format the timer the way we see in stopwatch (00: 00: 00)
- conditionally rendering the buttons
Start Function
The job of start function is to start the timer and keep incrementing until we reset or pause it.
For that, we will use setInterval
method. setInterval
runs as long as we don't stop it. It takes two parameters. A callback and time in milliseconds.
setInterval(func, time)
1000 ms = 1 second
const handleStart = () => {
setIsActive(true)
setIsPaused(true)
countRef.current = setInterval(() => {
setTimer((timer) => timer + 1)
}, 1000)
}
As soon we will hit the start button, isActive
and isPaused
will become true
and 1 will be added to timer values every second.
We set countRef
current property to the setInterval function, which means we set the timerId in variable countRef
, now we can use it in other functions.
We used countRef.current
to get the current value of the reference.
Pause Function
setInterval
keeps calling itself until clearInterval
is called.
In order to stop or pause the counter we need to use clearInterval
function. clearInterval needs one parameter that is id. We will pass countRef.current
as arguement in clearInterval
method.
const handlePause = () => {
clearInterval(countRef.current)
setIsPaused(false)
}
on pressing Pause button we will stop (not reset) the timer, and change isPaused
state from true
to false
.
Resume Function
const handleResume = () => {
setIsPaused(true)
countRef.current = setInterval(() => {
setTimer((timer) => timer + 1)
}, 1000)
}
On resuming the timer we will start the timer from where it was paused and change isPaused
from false
to true
.
Reset Function
const handleReset = () => {
clearInterval(countRef.current)
setIsActive(false)
setIsPaused(false)
setTimer(0)
}
Reset function will reset everything to its initial values. This button will not only stop the counter but also reset its value to 0.
Rendering Buttons Logic
Let's talk about start, pause, and resume button rendering logic.
Once the timer starts, the start button
will change into Pause
, if we pause the timer we will see Resume button
. This is how stopwatches work or you may say how we want this to work.
How do we know which button to show?
For that, we have already defined two keys in our state. One isActive
, and other one is isPaused
And both of them will be false initially.
If both keys will be false, we will show start button. It's obvious.
What happens in case of pause?
isActive will be true, isPaused will be false
Otherwise we will show resume button
We need to write nested if else
condition. Either we show start or pause/resume button.
Formatting Timer
Another tricky part of app is to show the timer in this manner 00:00:00
For seconds
const getSeconds = `0${(timer % 60)}`.slice(-2)
For minutes
const minutes = `${Math.floor(timer / 60)}`
const getMinutes = `0${minutes % 60}`.slice(-2)
For hours
const getHours = `0${Math.floor(timer / 3600)}`.slice(-2)
We made the formatTime
function for this, which returns seconds, minutes, and hours.
const formatTime = () => {
const getSeconds = `0${(timer % 60)}`.slice(-2)
const minutes = `${Math.floor(timer / 60)}`
const getMinutes = `0${minutes % 60}`.slice(-2)
const getHours = `0${Math.floor(timer / 3600)}`.slice(-2)
return `${getHours} : ${getMinutes} : ${getSeconds}`
}
In react the button has disabled
props that is false by default we can make it true by adding some logic. We made reset button disabled if the timer is set to 0 just by adding simple logic disabled={!isActive}
So far Complete Code
import React, { useState, useRef } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faClock } from '@fortawesome/free-regular-svg-icons'
import './App.css';
const element = <FontAwesomeIcon icon={faClock} />
const App = () => {
const [timer, setTimer] = useState(3595)
const [isActive, setIsActive] = useState(false)
const [isPaused, setIsPaused] = useState(false)
const increment = useRef(null)
const handleStart = () => {
setIsActive(true)
setIsPaused(true)
increment.current = setInterval(() => {
setTimer((timer) => timer + 1)
}, 1000)
}
const handlePause = () => {
clearInterval(increment.current)
setIsPaused(false)
}
const handleResume = () => {
setIsPaused(true)
increment.current = setInterval(() => {
setTimer((timer) => timer + 1)
}, 1000)
}
const handleReset = () => {
clearInterval(increment.current)
setIsActive(false)
setIsPaused(false)
setTimer(0)
}
const formatTime = () => {
const getSeconds = `0${(timer % 60)}`.slice(-2)
const minutes = `${Math.floor(timer / 60)}`
const getMinutes = `0${minutes % 60}`.slice(-2)
const getHours = `0${Math.floor(timer / 3600)}`.slice(-2)
return `${getHours} : ${getMinutes} : ${getSeconds}`
}
return (
<div className="app">
<h3>React Stopwatch {element}</h3>
<div className='stopwatch-card'>
<p>{formatTime()}</p>
<div className='buttons'>
{
!isActive && !isPaused ?
<button onClick={handleStart}>Start</button>
: (
isPaused ? <button onClick={handlePause}>Pause</button> :
<button onClick={handleResume}>Resume</button>
)
}
<button onClick={handleReset} disabled={!isActive}>Reset</button>
</div>
</div>
</div>
);
}
export default App;
Let's clean up our code
I have realized we can extract our state and methods to a custom hook. This will make our code clean and reusable.
useTimer hook
In src
folder, I have created one more folder hook
and within hook I created a file useTimer.js
useTimer hook returns our state and all four functions. Now we can use it wherever we want in our app.
import { useState, useRef } from 'react';
const useTimer = (initialState = 0) => {
const [timer, setTimer] = useState(initialState)
const [isActive, setIsActive] = useState(false)
const [isPaused, setIsPaused] = useState(false)
const countRef = useRef(null)
const handleStart = () => {
setIsActive(true)
setIsPaused(true)
countRef.current = setInterval(() => {
setTimer((timer) => timer + 1)
}, 1000)
}
const handlePause = () => {
clearInterval(countRef.current)
setIsPaused(false)
}
const handleResume = () => {
setIsPaused(true)
countRef.current = setInterval(() => {
setTimer((timer) => timer + 1)
}, 1000)
}
const handleReset = () => {
clearInterval(countRef.current)
setIsActive(false)
setIsPaused(false)
setTimer(0)
}
return { timer, isActive, isPaused, handleStart, handlePause, handleResume, handleReset }
}
export default useTimer
utils
We can make our code cleaner by writing our vanilla javascript functions into utils folder.
For that, within src
I created utils
folder, and inside utils I created index.js
file.
export const formatTime = (timer) => {
const getSeconds = `0${(timer % 60)}`.slice(-2)
const minutes = `${Math.floor(timer / 60)}`
const getMinutes = `0${minutes % 60}`.slice(-2)
const getHours = `0${Math.floor(timer / 3600)}`.slice(-2)
return `${getHours} : ${getMinutes} : ${getSeconds}`
}
Timer.js
I copied code from App.js
to Timer.js
and render Timer.js
inside App.js
This is how our folder structure will look like
import React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faClock } from '@fortawesome/free-regular-svg-icons'
import useTimer from '../hooks/useTimer';
import { formatTime } from '../utils';
const element = <FontAwesomeIcon icon={faClock} />
const Timer = () => {
const { timer, isActive, isPaused, handleStart, handlePause, handleResume, handleReset } = useTimer(0)
return (
<div className="app">
<h3>React Stopwatch {element}</h3>
<div className='stopwatch-card'>
<p>{formatTime(timer)}</p>
<div className='buttons'>
{
!isActive && !isPaused ?
<button onClick={handleStart}>Start</button>
: (
isPaused ? <button onClick={handlePause}>Pause</button> :
<button onClick={handleResume}>Resume</button>
)
}
<button onClick={handleReset} disabled={!isActive}>Reset</button>
</div>
</div>
</div>
);
}
export default Timer;
Doesn't it look cleaner now?
CSS
@import url("https://fonts.googleapis.com/css2?family=Quicksand:wght@500&display=swap");
body {
margin: 0;
font-family: "Quicksand", sans-serif;
background-color: #eceff1;
color: #010b40;
}
.app {
background-color: #0e4d92;
margin: 0 auto;
width: 300px;
height: 200px;
position: relative;
border-radius: 10px;
}
h3 {
color: white;
text-align: center;
padding-top: 8px;
letter-spacing: 1.2px;
font-weight: 500;
}
p {
font-size: 28px;
}
.stopwatch-card {
position: absolute;
text-align: center;
background-color: white;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
width: 325px;
height: 130px;
top: 110px;
left: 50%;
transform: translate(-50%, -50%);
}
button {
outline: none;
background: transparent;
border: 1px solid blue;
padding: 5px 10px;
border-radius: 7px;
color: blue;
cursor: pointer;
}
.buttons {
display: flex;
justify-content: space-evenly;
width: 150px;
margin: 0 auto;
margin-top: 5px;
}
I want a little feedback if you would like to read the next article with typescript?
Since typescript is evolving and startups are preferring those who can type javascript with typescript.
In next part, we will transform this app to the Pomodoro clock.
Codepen Demo
Posted on July 19, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.