How to Build a Pomodoro Timer in React

vaatiesther

vaatiesther

Posted on September 23, 2024

How to Build a Pomodoro Timer in React

One of the best ways to master any framework is by getting your hands dirty by building projects. In this tutorial, we will learn step-by-step how to build a Pomodoro timer in React

Pomodoro timer

Prerequisites

  • Foundational Javascript Knowledge
  • Basics of React

Basics of React

React makes it easy to build applications. It uses the component architecture, which makes it easy to build apps. If you wanted to build this application in pure JavaScript, you would have to get and manipulate the DOM elements.

With React, you only need to update the state or props and React efficiently handles updating the DOM.

React uses components to build different parts of an application. These components use JSX syntax, which is easier to read and write.

Create a new React App with Vite

There are several ways to set up a new React app. You can use tools like Vite or Create React App. In this tutorial, we will use Vite. Vite is a modern build tool that allows developers to quickly set up and organize React applications.

Issue this command to create a new app called pomodoro with vite



npm create vite@latest pomodoro


Enter fullscreen mode Exit fullscreen mode

From your terminal, select React as the framework.

vite

In the next step select JavaScript

vite

Now cd into the pomodoro folder and issue the npm install command to install dependencies:



cd pomodoro
npm install


Enter fullscreen mode Exit fullscreen mode

Lastly, issue this command to run the server



 npm run dev


Enter fullscreen mode Exit fullscreen mode

Your app should now be running at http://localhost:5173/.

The project comes with some starter code. In your App.js file, delete the div inside the return statement of the App component. You should now be working with a clean slate, like this:



import { useState } from 'react'
import './App.css'

function App() {

  return (
    <>

    </>
  )
}

export default App


Enter fullscreen mode Exit fullscreen mode

Just a recap, in the code above we have an App component. React components should be written in camelCase and exported either as default or named exports.

The App component currently returns an empty JSX element. Inside the return statement is where we will add the HTML structure for the Pomodoro timer. For now, we will use just one component.

Update the file as shown below:



import { useState } from "react";
import "./App.css";

function App() {
  return (
    <>
      <div className="wrapper">
          <h1>Pomodoro Timer</h1>

          <div className="timer-display">
            <span>25</span>
            <span>:</span>
            <span>00</span>
          </div>

          <div className="buttons">
            <button>START</button>
            <button>STOP</button>
          </div>
        </div>
    </>
  );
}

export default App;


Enter fullscreen mode Exit fullscreen mode

Add the following styles in the index.css file



body {
  font-family: Arial, sans-serif;
  text-align: center;
  background-color: #fff;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}

.wrapper {
  display: inline-block;
  border: 2px solid #05011d;
  padding: 20px;
  width: 600px;
  box-sizing: border-box;
}
h1 {
  font-size: 24px;
  margin-top: 20px;
}

.timer-display {
  font-size: 48px;
  margin-top: 20px;
}
.buttons {
  margin-top: 20px;
}
.start,
.stop {

  color: #fff;
  border: none;
  border-radius: 5px;
  padding: 10px 20px;
  margin: 10px;
  cursor: pointer;
}
.start{
  background-color: #53ae5e;
}
.stop{
  background-color: #d90f26;
}


Enter fullscreen mode Exit fullscreen mode

Now we have a great-looking UI.

pomodoro timer

The first thing you might notice is that the time is hardcoded. To make it dynamically updatable, we need to set the initial time (e.g., 25 minutes) and allow it to be adjusted. We will start by storing the original time in a state variable.

The useState Hook

The useState Hook allows you to add state to functional components. This will allow us to declare the time and update it whenever the timer starts running.

The useState hook looks like this:



const [state, setState] = useState(initialState);


Enter fullscreen mode Exit fullscreen mode
  • state: The current state value.
  • setState: A function to update the state.
  • initialState: The initial value of the state.

In our case, we want the initial timer value to be 1500 seconds(25 mins). We will then use the setState function to update the new state as the timer continues running.

Let's create our states for the initial time. To make it easier to work with time, we will convert the initial time to seconds.



  const[timeLeft, settimeLeft] = useState(1500);


Enter fullscreen mode Exit fullscreen mode

A general rule of hooks in react is that they should be declared at the top level of the component. Hooks should also not be declared inside loops, conditions, or nested functions. 

To display the time in minutes and seconds, let's convert the time back to minutes and seconds and update the span elements in the timer display. 



<div className="timer-display">
    <span>{String(Math.floor(timeLeft / 60)).padStart(2, "0")}</span>
    <span>:</span>
    <span>{String(timeLeft % 60).padStart(2, "0")}</span>
  </div>


Enter fullscreen mode Exit fullscreen mode

The padStart() function adds leading zeros to ensure consistency. For example, if the remaining minutes are 9, the timer will show 09. The same formatting is applied to the seconds.

To start the timer, we need to create a function startTimer which will be called when the START button is clicked, This function will then decrement the time by 1 second. 

Create a function called StartTimer.



function App() {
 const [timeLeft, settimeLeft] = useState(1500);
 function startTimer(){
  // the rest of the code .....

  }}


Enter fullscreen mode Exit fullscreen mode

Working with Time in JavaScript

When working with time in JavaScript, it's ideal to work with seconds and milliseconds. To decrement or increment the timer, you use the setInterval() method which repeatedly calls a function at the specified time interval

From MDN docs, this is the definition:

The setInterval() method, offered on the Window and WorkerGlobalScope interfaces, repeatedly calls a function or executes a code snippet, with a fixed time delay between each call.
This method returns an interval ID which uniquely identifies the interval, so you can remove it later by calling clearInterval().

Using setInterval in pure JavaScript

If we were using pure javaScript, we would use setInterval() to decrement the timer after 1 second like this:



const interval = setInterval(() => {
  // decrement timer
}, 1000);


Enter fullscreen mode Exit fullscreen mode

This will not work in React because of the way React rerenders states and components.

Using setInterval in React

React components re-render based on changes in props and state. If we use setInterval directly, it will not reflect the recent state since state updates are asynchronous. 

To solve this issue, we need a way of updating the interval on every re-render, we will use useRef which will keep the state of the interval intact until it is cleared.

At the top of the App component file,import the useRef hook and, just after the useState hook, store the interval ID using useRef as shown below.



import { useState,useRef } from "react";
function App() {
  const [timeLeft, settimeLeft] = useState(1500);
  const intervalRef = useRef(null);
  /// the rest of the code



Enter fullscreen mode Exit fullscreen mode

Start Pomodoro Timer

To start the Pomodoro Timer, add this code to the startTimer function.



function startTimer() {
    intervalRef.current = setInterval(() => {
      settimeLeft((prevTimeLeft) => {
        return prevTimeLeft - 1});
    }, 1000);
  }


Enter fullscreen mode Exit fullscreen mode

Since states are asynchronous, when we update the timeLeft, we have to use the functional update form : settimeLeft(prevTimeLeft=> prevTimeLeft -1);This ensures that we have an accurate representation of the recent state.

Now let's bind the START button to the startTimer function so that it gets called when the button is clicked.



 <button onClick={startTimer}>START</button>


Enter fullscreen mode Exit fullscreen mode

In React, you have to remember that event listeners are always camel-cased. Additionally, ensure that the function attached to the event listener is referenced rather than invoking it. (e.g., onClick={startTimer}) rather than invoking it directly (e.g., onClick={startTimer()})

When you click the START button, the timer will decrement as expected.

pomodoro timer

However, there is a potential flaw in our timer, when the timer reaches 0, it will start showing negative values. We need to create a condition to clear the timer when the timeLeft equals 0.
Update the startTimer function as follows:



  function startTimer() {
    intervalRef.current = setInterval(() => {
      settimeLeft((prevTimeLeft) => {
        if(prevTimeLeft<=0){
          clearInterval(intervalRef.current)
          intervalRef.current =null
          return 0
        }
        return prevTimeLeft - 1});

    }, 1000);
  }


Enter fullscreen mode Exit fullscreen mode

After the timer reaches 0, we clear the interval and update the value of intervalRef to the original null value. The timer works as expected.

Stop Pomodoro Timer

The next functionality is the ability to stop a timer. Create a function stopTimer and add the code below.



function stopTimer(){
  clearInterval(intervalRef.current)

}


Enter fullscreen mode Exit fullscreen mode

To stop the timer, all we need to do is clear the interval. 

Conclusion

This tutorial has covered how to create a functional Pomodoro Timer in React. The code can be found in this repo:

Thanks for reading

The best way to master JavaScript is by building projects.
Subscribe to the Practical JavaScript Newsletter and level up your JavaScript skills.

💖 💪 🙅 🚩
vaatiesther
vaatiesther

Posted on September 23, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related