Build a Simple React JS Interactive Web-Based Pomodoro Timer: A Beginner’s Guide

In the fast-paced world we live in, time management is a crucial skill. Whether you’re a student, a professional, or simply someone trying to get more done, the ability to focus and maintain productivity is invaluable. The Pomodoro Technique is a time management method that uses a timer to break down work into intervals, traditionally 25 minutes in length, separated by short breaks. This simple yet effective technique can significantly boost your concentration and help you achieve your goals. In this tutorial, we’ll build a React JS Pomodoro Timer, a web application that implements this technique.

Why Build a Pomodoro Timer?

There are several reasons why building a Pomodoro Timer in React JS is a great project for developers of all levels, especially beginners to intermediate developers:

  • Practical Application: You’ll create a tool that you can use daily to improve your productivity.
  • Core React Concepts: You’ll gain hands-on experience with fundamental React concepts such as state management, component lifecycle, and event handling.
  • User Interface (UI) Design: You’ll learn how to create a simple and intuitive user interface using HTML, CSS, and React components.
  • Problem-Solving: You’ll encounter and solve real-world problems related to time manipulation and user interaction.
  • Expandability: You can easily extend this project with additional features like task tracking, customization options, and more.

Prerequisites

Before we begin, make sure you have the following prerequisites:

  • Node.js and npm (or yarn) installed: These are essential for managing your project’s dependencies and running the React development server.
  • Basic understanding of HTML, CSS, and JavaScript: Familiarity with these technologies is crucial for understanding the code and making modifications.
  • A code editor: Choose your favorite code editor, such as Visual Studio Code, Sublime Text, or Atom.

Step-by-Step Guide to Building a React Pomodoro Timer

1. Setting Up the Project

First, let’s set up a new React project using Create React App. Open your terminal or command prompt and run the following command:

npx create-react-app pomodoro-timer
cd pomodoro-timer

This command creates a new React application named “pomodoro-timer” and navigates you into the project directory.

2. Project Structure and Initial Files

After the project is created, your directory structure should look something like this:


pomodoro-timer/
  ├── node_modules/
  ├── public/
  ├── src/
  │   ├── App.js
  │   ├── App.css
  │   ├── index.js
  │   ├── index.css
  │   └── App.test.js
  ├── package.json
  └── README.md

The core files we’ll be working with are:

  • src/App.js: This is the main component of our application.
  • src/App.css: This is where we’ll add our CSS styles.
  • src/index.js: This is the entry point of our application.

3. Cleaning Up the Default App.js

Open src/App.js and remove the boilerplate code. Replace it with the following:

import React, { useState, useEffect } from 'react';
import './App.css';

function App() {
  // State variables will go here
  return (
    <div className="App">
      <header className="App-header">
        <h1>Pomodoro Timer</h1>
        {/* Timer display and controls will go here */}
      </header>
    </div>
  );
}

export default App;

This sets up the basic structure for our application, including the import of React, useState, useEffect and the App.css file. We’ve added a header with the title “Pomodoro Timer.”

4. Setting Up the CSS (App.css)

Open src/App.css and add the following CSS to style the app. This provides basic styling for the timer, button, and header. You can customize this to your liking.

.App {
  text-align: center;
  font-family: sans-serif;
}

.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.timer-display {
  font-size: 4em;
  margin-bottom: 20px;
}

.button {
  padding: 10px 20px;
  font-size: 1.2em;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  margin: 10px;
}

.button-start {
  background-color: #4CAF50;
  color: white;
}

.button-stop {
  background-color: #f44336;
  color: white;
}

.button-reset {
  background-color: #008CBA;
  color: white;
}

5. Implementing State Variables

Inside the App component, we need to declare state variables to manage the timer’s behavior:

  • timeLeft: This variable holds the remaining time in seconds.
  • isRunning: This boolean variable indicates whether the timer is running or paused.
  • timerType: This variable holds the current timer type (e.g., “pomodoro” or “break”).
  • pomodoroLength: This variable holds the pomodoro session length in seconds.
  • breakLength: This variable holds the break length in seconds.

Add the following code inside the App component, above the return statement:

  const [timeLeft, setTimeLeft] = useState(25 * 60); // 25 minutes in seconds
  const [isRunning, setIsRunning] = useState(false);
  const [timerType, setTimerType] = useState('pomodoro');
  const [pomodoroLength, setPomodoroLength] = useState(25 * 60);
  const [breakLength, setBreakLength] = useState(5 * 60);

6. Creating the Timer Display

Inside the <header> element, add the following code to display the timer.


  <div className="timer-display">
    {/* Format the time into minutes:seconds */}
    {String(Math.floor(timeLeft / 60)).padStart(2, '0')}:{
      String(timeLeft % 60).padStart(2, '0')
    }
  </div>

This code formats the timeLeft state variable into a “minutes:seconds” format, using padStart(2, '0') to ensure that minutes and seconds always have two digits (e.g., “05” instead of “5”).

7. Implementing Timer Controls (Start/Stop/Reset)

Add the following buttons within the <header> element, below the timer display.


  <div>
    <button className="button button-start" onClick={() => setIsRunning(true)}>
      Start
    </button>
    <button className="button button-stop" onClick={() => setIsRunning(false)}>
      Stop
    </button>
    <button className="button button-reset" onClick={resetTimer}>
      Reset
    </button>
  </div>

We’ll need to define the resetTimer function. Add the following function inside the App component, above the return statement:

  const resetTimer = () => {
    setIsRunning(false);
    setTimeLeft(pomodoroLength);
    setTimerType('pomodoro');
  };

The `resetTimer` function resets the timer to the initial values, stopping the timer and setting the `timeLeft` back to the pomodoro length.

8. Implementing the Timer Logic with useEffect

The useEffect hook allows us to run side effects in functional components. We’ll use it to update the timer every second.

Add the following useEffect hook inside the App component, above the return statement:


  useEffect(() => {
    let interval;

    if (isRunning && timeLeft > 0) {
      interval = setInterval(() => {
        setTimeLeft((prevTime) => prevTime - 1);
      }, 1000);
    }

    if (timeLeft === 0) {
      // Timer finished, switch to break or pomodoro
      if (timerType === 'pomodoro') {
        setTimeLeft(breakLength);
        setTimerType('break');
      } else {
        setTimeLeft(pomodoroLength);
        setTimerType('pomodoro');
      }
      setIsRunning(false);
    }

    return () => clearInterval(interval); // Cleanup on unmount
  }, [isRunning, timeLeft, pomodoroLength, breakLength, timerType]);

Let’s break down this code:

  • `useEffect(() => { … }, [isRunning, timeLeft, pomodoroLength, breakLength, timerType]);`: This hook runs whenever `isRunning`, `timeLeft`, `pomodoroLength`, `breakLength`, or `timerType` changes.
  • `if (isRunning && timeLeft > 0)`: If the timer is running and time is left, this code starts an interval using `setInterval`. The interval decrements `timeLeft` by 1 every second.
  • `if (timeLeft === 0)`: This block checks if the timer has reached zero. If so, it switches between “pomodoro” and “break” timers, and resets `timeLeft` to the appropriate value.
  • `return () => clearInterval(interval);`: This is the cleanup function. It clears the interval when the component unmounts or when any of the dependency variables change, preventing memory leaks and unwanted behavior.

9. Implementing Timer Length Controls

Add the following code before the timer display:


  <div>
    <label htmlFor="pomodoro-length">Pomodoro Length (minutes): </label>
    <input
      type="number"
      id="pomodoro-length"
      value={pomodoroLength / 60}
      onChange={(e) => {
        const newLength = parseInt(e.target.value, 10) * 60;
        setPomodoroLength(newLength > 0 ? newLength : 0);
        setTimeLeft(newLength);
      }}
    />
    <label htmlFor="break-length">Break Length (minutes): </label>
    <input
      type="number"
      id="break-length"
      value={breakLength / 60}
      onChange={(e) => {
        const newLength = parseInt(e.target.value, 10) * 60;
        setBreakLength(newLength > 0 ? newLength : 0);
        if (timerType === 'break') {
          setTimeLeft(newLength)
        }
      }}
    />
  </div>

Here’s how this code works:

  • Two <input> elements allow the user to set the pomodoro and break lengths in minutes.
  • The value of the input fields is bound to the pomodoroLength and breakLength state variables, divided by 60 to display minutes.
  • The onChange event handlers update the corresponding state variables when the input values change.
  • Input validation is added to prevent negative numbers
  • When the break length is changed while the break timer is running, the time left is also updated.

10. Complete Code (App.js)

Here’s the complete code for src/App.js:

import React, { useState, useEffect } from 'react';
import './App.css';

function App() {
  const [timeLeft, setTimeLeft] = useState(25 * 60); // 25 minutes in seconds
  const [isRunning, setIsRunning] = useState(false);
  const [timerType, setTimerType] = useState('pomodoro');
  const [pomodoroLength, setPomodoroLength] = useState(25 * 60);
  const [breakLength, setBreakLength] = useState(5 * 60);

  const resetTimer = () => {
    setIsRunning(false);
    setTimeLeft(pomodoroLength);
    setTimerType('pomodoro');
  };

  useEffect(() => {
    let interval;

    if (isRunning && timeLeft > 0) {
      interval = setInterval(() => {
        setTimeLeft((prevTime) => prevTime - 1);
      }, 1000);
    }

    if (timeLeft === 0) {
      // Timer finished, switch to break or pomodoro
      if (timerType === 'pomodoro') {
        setTimeLeft(breakLength);
        setTimerType('break');
      } else {
        setTimeLeft(pomodoroLength);
        setTimerType('pomodoro');
      }
      setIsRunning(false);
    }

    return () => clearInterval(interval); // Cleanup on unmount
  }, [isRunning, timeLeft, pomodoroLength, breakLength, timerType]);

  return (
    <div className="App">
      <header className="App-header">
        <h1>Pomodoro Timer</h1>
        <div>
          <label htmlFor="pomodoro-length">Pomodoro Length (minutes): </label>
          <input
            type="number"
            id="pomodoro-length"
            value={pomodoroLength / 60}
            onChange={(e) => {
              const newLength = parseInt(e.target.value, 10) * 60;
              setPomodoroLength(newLength > 0 ? newLength : 0);
              setTimeLeft(newLength);
            }}
          />
          <label htmlFor="break-length">Break Length (minutes): </label>
          <input
            type="number"
            id="break-length"
            value={breakLength / 60}
            onChange={(e) => {
              const newLength = parseInt(e.target.value, 10) * 60;
              setBreakLength(newLength > 0 ? newLength : 0);
              if (timerType === 'break') {
                setTimeLeft(newLength)
              }
            }}
          />
        </div>
        <div className="timer-display">
          {String(Math.floor(timeLeft / 60)).padStart(2, '0')}:{
            String(timeLeft % 60).padStart(2, '0')
          }
        </div>
        <div>
          <button className="button button-start" onClick={() => setIsRunning(true)}>
            Start
          </button>
          <button className="button button-stop" onClick={() => setIsRunning(false)}>
            Stop
          </button>
          <button className="button button-reset" onClick={resetTimer}>
            Reset
          </button>
        </div>
      </header>
    </div>
  );
}

export default App;

Testing and Running the Application

To run your application, execute the following command in your terminal:

npm start

This command starts the development server, and your Pomodoro Timer should open automatically in your web browser (usually at http://localhost:3000). You can now test the timer by starting, stopping, and resetting it. Try changing the Pomodoro and Break lengths, and ensure the timer switches between them correctly.

Common Mistakes and How to Fix Them

Here are some common mistakes and their solutions:

  • Incorrect Time Formatting: The timer display might show the time in an incorrect format. Make sure you are using padStart(2, '0') to format the minutes and seconds with leading zeros.
  • Timer Not Updating: If the timer doesn’t update, double-check your useEffect hook dependencies. The hook should depend on isRunning and timeLeft.
  • Memory Leaks: Without a proper cleanup function in the useEffect hook (clearInterval(interval)), you might encounter memory leaks. Always clear the interval when the component unmounts or when the dependencies change.
  • Incorrect State Updates: Ensure you are correctly updating the state variables using the set... functions. Incorrectly updating state can lead to unexpected behavior.
  • Input Validation Issues: Ensure your input fields handle invalid input (e.g., non-numeric values or negative numbers). Use parseInt and add validation to prevent incorrect values from being set.

Key Takeaways and Summary

  • State Management: You learned how to manage the state of the timer using the useState hook.
  • Event Handling: You implemented event handlers for the start, stop, and reset buttons.
  • Component Lifecycle: You used the useEffect hook to handle the timer’s behavior, including starting, stopping, and switching between Pomodoro and break intervals.
  • User Interface: You created a simple but functional user interface with HTML, CSS, and React components.
  • Problem Solving: You gained experience in breaking down a problem (time management) into smaller, manageable components.

FAQ

Here are some frequently asked questions about building a Pomodoro Timer in React:

  1. Can I customize the timer sounds? Yes, you can add sound effects to your timer using the HTML <audio> element or the Web Audio API.
  2. How can I add task tracking functionality? You can add task tracking by creating a list of tasks and associating them with the Pomodoro sessions. This can be achieved using an array of objects to store tasks and their completion status.
  3. Can I add a setting to change the session lengths? Yes, you can add input fields or a settings panel to allow users to customize the Pomodoro and break lengths.
  4. How can I deploy this application? You can deploy your React application to platforms like Netlify, Vercel, or GitHub Pages.
  5. How can I add notifications when the timer ends? You can use the Web Notifications API to display notifications when the timer completes a Pomodoro session or break.

Building a Pomodoro Timer in React JS is an excellent way to learn and practice essential React concepts. You’ve now built a functional timer that you can use to improve your productivity. You can further enhance this project by adding more features. You can customize the look and feel, add sound effects, or integrate task management. The possibilities are endless. Keep experimenting, learning, and building!