DEV Community

Cover image for How to develop a Stopwatch in React JS with custom hook
Abdul Basit
Abdul Basit

Posted on

How to develop a Stopwatch in React JS with custom hook

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;
Enter fullscreen mode Exit fullscreen mode

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)

Alt Text

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

For minutes

 const minutes = `${Math.floor(timer / 60)}`
 const getMinutes = `0${minutes % 60}`.slice(-2)
Enter fullscreen mode Exit fullscreen mode

For hours

const getHours = `0${Math.floor(timer / 3600)}`.slice(-2)
Enter fullscreen mode Exit fullscreen mode

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}`
  }
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}`
}
Enter fullscreen mode Exit fullscreen mode

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
Alt Text

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;
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (16)

Collapse
 
jamiyand profile image
JamiyanD

tnk u it is easily understand

Collapse
 
killcodex profile image
Aaquib Ahmed

thanks, bro you saved me !! I read a lot of articles but it was very simple and easy !!

Collapse
 
killcodex profile image
Aaquib Ahmed

Bro can you write another article on ref method?

Collapse
 
abdulbasit313 profile image
Abdul Basit

Sure. What do you want to achieve with ref?

Thread Thread
 
killcodex profile image
Aaquib Ahmed

I want to understand the actual use of ref and why it should not use frequently

Thread Thread
 
abdulbasit313 profile image
Abdul Basit

in react we use useRef to get the dom element. Let say you want to find out scroll position and things like this.

Collapse
 
gayatrea profile image
gautam pal

Easy to understand

Collapse
 
shahmir049 profile image
Shahmir Faisal

Wow. Great article. Nice explanation.

Collapse
 
abdulbasit313 profile image
Abdul Basit

Thanks Shahmir... It means to me.

Collapse
 
sunflower profile image
sunflowerseed • Edited

although, is it using setInterval(fn, 1000) to count as 1 second? What if it is 1.016 seconds because JavaScript can delay and not invoke the handler until 1.016 seconds later? Then after a while, it can be off... try it with your iPhone's stopwatch and see if after 30 minutes or an hour, whether your clock is off by a few seconds.

Also if you start the time and go off exercise, come back and the computer is in a sleep, then you wake up the computer and if the processor wasn't running, your timer will be totally off?

Collapse
 
keomalima profile image
Keoma Lima

Hello! I've adapted your code on my react native application but as soon as my screen is blocked, the timer stops running, do you have any idea on how I can keep the timer running even with the screen blocked? Thanks a lot

Collapse
 
abdulbasit313 profile image
Abdul Basit

Though I haven't implemented it, but there are some events in react native that can be called on blocking the screen or going back, so on that event, you can save the timer in async storage. There can be multiple ways to achieve it...

Collapse
 
satish_kumar profile image
satish kumar

Thank you so much its a good example, ease and clean.

Collapse
 
zeealik profile image
Zeeshan Ali Khan

It helped me a lot, was making check-in/out screen in my app and it really helped me out in this.

Collapse
 
abdulbasit313 profile image
Abdul Basit

I am glad to know.

Collapse
 
miguelhv profile image
Miguel

Nice explanation, I hadn't used custom hooks previously!