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
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
From your terminal, select React as the framework.
In the next step select JavaScript
Now cd into the pomodoro folder and issue the npm install command to install dependencies:
cd pomodoro
npm install
Lastly, issue this command to run the server
npm run dev
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
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;
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;
}
Now we have a great-looking UI.
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);
-
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);
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>
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 .....
}}
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 theWindow
andWorkerGlobalScope
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 callingclearInterval()
.
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);
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
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);
}
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>
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.
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);
}
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)
}
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.
Top comments (0)