Mitchell Hillman

React Timer

I made a timer in react with useEffect, setInterval and using feathersjs for the backend. Here I’ll show the main pieces and then how they all fit together.

Timer

useEffect creates an Interval when the timer has been started.

useEffect(() => {
  let interval = null;
  if (timerStart) {
    interval = setInterval(() => {
      // set the state for current time
    }, 1000);
  } else if (!timerStart) {
    clearInterval(interval);
  }
  return () => clearInterval(interval);
});

Start and Stop functions

The timerStart needs to be persistent so that the timer is not reset when the browser is refreshed. In this case, a feathers service is called to save the start time and total elapsed time to a database. window.localStorage could also work, but only if the application was intended to be used in a single browser runtime. I wanted the timer to be synced across devices so it could be started and stopped from anywhere. Therefore, a server and database was required.

const handleStartTaskTimer = (obj) => {
  // update the task with the new start time
  client.service("tasks").update(obj._id, {
    ...obj,
    timerStart: new Date(),
  });
};

When the “Stop” button is clicked a new time entry is pushed to the timeLog and the timer is cleared.

const handleStopTaskTimer = (obj) => {
  setDiff(0);
  const newTimeLog = obj.timeLog;
  newTimeLog.push([obj.timerStart, new Date()]);
  client.service("tasks").update(obj._id, {
    ...obj,
    timeLog: newTimeLog,
    timerStart: undefined,
  });
};

Time logic

I used moment to compare the timerStart with the current time. The primary benefit of this dependency is to make use of the moment-duration-format plugin. This plugin made the display of elapsed time much easier (and less buggy) than if I did it myself. Maybe I’ll write a formatting library in the future :)

// get the difference in time between now and when the timer started
const diff = moment().diff(timerStart) / 1000;
// get the duration as measured in seconds and then format
const duration = moment
  .duration(diff, "seconds")
  .format("hh:mm:ss", { trim: false });

Full component

import React, { useEffect, useState } from "react";
import moment from "moment";
import momentDuration from "moment-duration-format";
import client from "./feathers";

momentDuration(moment);

function Timer({ task }) {
  const { timerStart } = task;
  const [diff, setDiff] = useState(0);

  useEffect(() => {
    let interval = null;
    if (timerStart) {
      interval = setInterval(() => {
        setDiff(moment().diff(timerStart) / 1000);
      }, 1000);
    } else if (!timerStart) {
      clearInterval(interval);
    }
    return () => clearInterval(interval);
  });

  const handleStartTaskTimer = (obj) => {
    client.service("tasks").update(obj._id, {
      ...obj,
      timerStart: new Date(),
    });
  };

  const handleStopTaskTimer = (obj) => {
    setDiff(0);
    const newTimeLog = obj.timeLog;
    newTimeLog.push([obj.timerStart, new Date()]);
    client.service("tasks").update(obj._id, {
      ...obj,
      timeLog: newTimeLog,
      timerStart: undefined,
    });
  };

  const duration = moment
    .duration(diff, "seconds")
    .format("hh:mm:ss", { trim: false });

  return (
    <div className="timer">
      <div className={`time ${timerStart ? "active" : ""}`}>
        {timerStart ? duration : "00:00:00"}
      </div>
      <button
        type="button"
        disabled={timerStart}
        onClick={() => {
          handleStartTaskTimer(task);
        }}
      >
        🔴 Start
      </button>
      <button
        disabled={!timerStart}
        type="button"
        onClick={() => {
          handleStopTaskTimer(task);
        }}
      >
        ⬛ Stop
      </button>
    </div>
  );
}

export default Timer;