Stopwatch – React

Build a stopwatch widget which can measure how much time has passed. It shows the current timer and has two buttons underneath: “Start/Stop” and “Reset”.

Requirements

Start/Stop Button: Starts/stops the timer depending on whether the timer is runnin…


This content originally appeared on DEV Community and was authored by MING

Build a stopwatch widget which can measure how much time has passed. It shows the current timer and has two buttons underneath: "Start/Stop" and "Reset".

Requirements

  • Start/Stop Button: Starts/stops the timer depending on whether the timer is running.
  • Reset: Resets the timer to 0 and stops the timer.
  • The timer shows the number of seconds elapsed, down to the millisecond.
    • Clicking on the timer should start/stop the timer. The Start/Stop button's label should update accordingly as well.
    • It'd be a nice optional addition to format the time to display in hh:mm:ss:ms format.

You are free to exercise your creativity to style the appearance of the stopwatch. You can try out Google's stopwatch widget for inspiration and an example.

Image description

Solution:

This question looks simple on first glance but is actually more complex than it seems. Note that setInterval's delay parameter is unreliable. The actual amount of time that elapses between calls to the callback may be longer than the given delay due to various reasons. Because of this behavior, we cannot assume that each time the interval callback is fired, the same duration as passed. We will need to read the current time within the callback code to ensure that we are using the most updated timings.

State
The tricky part of this question is deciding what goes into the component state and how to manage them. We need a few states:

  • totalDuration: Total time that has passed so far.
  • timerId: Timer ID of the currently running interval timer, or null if there's no currently running timer.
  • lastTickTiming: This is the time that the last interval callback has run. We will keep incrementing the totalDuration by the delta between the current time (Date.now()) and the lastTickTiming. Using this approach, the totalDuration will still be accurate even if the callbacks run at irregular intervals. We use useRef to create this value since it's not used in the render code.

Since there are a few buttons in the requirements that has duplicate functionality, we should define these functionality as a few functions that will be triggered by the buttons:

startTimer
This function kicks off the timer and updates the totalDuration value each time the setInterval callback is run with the delta between the last update time (lastTickTiming) and the current time. We use a interval timing of 1ms since stopwatches are very time sensitive and millisecond-level precision is desired.

stopInterval
A simple function to stop the interval timer from running (via clearInterval) and clear the current timerId. This is being used by the "Stop" button and "Reset" button.

resetTimer
We want to reset the component to its initial state in this function. It stops the interval timer by calling stopInterval() and also resets the total duration to 0. It's not important to reset the value of lastTickTiming because it will be set at the start ofstartTimer(), before the first interval callback is executed. Used by the "Reset" button.

toggleTimer
A function to toggle between calling stopInterval() and startTimer() depending on whether there's a current timer. Used by the time display and the "Start"/"Stop" button.

Accessibility
People who are unfamiliar with a11y will add an onClick/'click' event to the DOM element rendering the time (usually a

) and consider it complete. However, this the timer is not a11y-friendly just by doing so. Some might add tabIndex="0" (to allow focus) and role="button" to the element, which certainly improves the a11y, but is not the best.

For the best a11y, we can and should use a <button> to render the timing, which comes with additional a11y benefits like focus and keyboard support. By using a <button>, you get automatic focus support (be able to use Tab to focus onto the timer) and keyboard support (hit the Spacebar to start/stop the timer). The latter will not be possible without custom code to add key event listeners to non-interactive elements.

User Experience
user-select: none is added to the timer so that the digits aren't selected if a user double clicks on them. Selecting the digits is usually not desired.

StopWatchWrapper.js

import React from 'react';
import { StopWatch } from './StopWatch';

export const StopWatchWrapper = () => {
  return <StopWatch />;
};

StopWatch.js

import React, { useState, useRef } from 'react';
import './stopwatch.css';

const MS_IN_SECOND = 1000;
const SECONDS_IN_MINUTE = 60;
const MINUTES_IN_HOUR = 60;
const MS_IN_HOUR = MINUTES_IN_HOUR * SECONDS_IN_MINUTE * MS_IN_SECOND;
const MS_IN_MINUTE = SECONDS_IN_MINUTE * MS_IN_SECOND;

const padTwoDigit = (number) => (number >= 10 ? String(number) : `0${number}`);
/* key point is this function: */
const formatTime = (timeParam) => {
  let time = timeParam;
  const parts = {
    hours: 0,
    minutes: 0,
    seconds: 0,
    ms: 0,
  };

  if (time > MS_IN_HOUR) {
    parts.hours = Math.floor(time / MS_IN_HOUR);
    time %= MS_IN_HOUR;
  }

  if (time > MS_IN_MINUTE) {
    parts.minutes = Math.floor(time / MS_IN_MINUTE);
    time %= MS_IN_MINUTE;
  }

  if (time > MS_IN_SECOND) {
    parts.seconds = Math.floor(time / MS_IN_SECOND);
    time %= MS_IN_SECOND;
  }

  parts.ms = time;

  return parts;
};

export const StopWatch = () => {
  const lastTickTiming = useRef(null); // use `useRef` to create this value since it's not used in the render code.
  const [totalDuration, setTotalDuration] = useState(0);
  const [timerId, setTimerId] = useState(null); // Timer ID of the active interval, if one is running.

  const isRunning = timerId != null; // Derived state to determine if there's a timer running.

  const startTimer = () => {
    lastTickTiming.current = Date.now(); // 用于记录上次setInterval的callback停在哪儿了(记录了ms)

    //下面这个setInterval一直run, 每隔1ms run一次callback, 然后根据 之前的现在的时间-上次记录的ms = passedtime, 再去更新totalDuration
    const timer = window.setInterval(() => {
      const now = Date.now();
      const timePassed = now - lastTickTiming.current;
      // Use the callback form of setState to ensure we are using the latest value of duration.
      setTotalDuration((duration) => duration + timePassed);
      lastTickTiming.current = now; // update lastTickTiming
    }, 1);

    setTimerId(timer);
  };

  const stopInterval = () => {
    window.clearInterval(timerId);
    setTimerId(null);
  };

  const resetTimer = () => {
    stopInterval();
    setTotalDuration(0);
  };

  const toggleTimer = () => {
    if (isRunning) stopInterval();
    else startTimer();
  };

  const formattedTime = formatTime(totalDuration);
  // console.log(formattedTime);
  return (
    <div>
      <button
        className='time'
        onClick={() => {
          toggleTimer();
        }}
      >
        {formattedTime.hours > 0 && (
          <span>
            <span className='time-number'>{formattedTime.hours}</span>
            <span className='time-unit'>h</span>
          </span>
        )}

        {formattedTime.minutes > 0 && (
          <span>
            <span className='time-number'>{formattedTime.minutes}</span>
            <span className='time-unit'>m</span>
          </span>
        )}

        <span>
          <span className='time-number'>{formattedTime.seconds}</span>
          <span className='time-unit'>s</span>
        </span>

        <span className='time-number time-number--small'>
          {padTwoDigit(Math.floor(formattedTime.ms / 10))}
        </span>
      </button>

      <div>
        <button
          onClick={() => {
            toggleTimer();
          }}
        >
          {isRunning ? 'Stop' : 'Start'}
        </button>
        <button
          onClick={() => {
            resetTimer();
          }}
        >
          Reset
        </button>
      </div>
    </div>
  );
};

timewatch.css

.time {
  align-items: baseline;
  background-color: transparent;
  border: none;
  cursor: pointer;
  display: flex;
  gap: 16px;
  user-select: none;
}

.time-unit {
  font-size: 24px
}

.time-number {
  font-size: 62px;
}

.time-number--small {
  font-size: 36px;
}

Note: above logic can works as well for timer countdown. the most important thing should be changed are:
1.const [totalDuration, setTotalDuration] = useState(maxMinutes * MS_IN_MINUTE);, the maxMinutes is the Timer component's props, which can be decided by user's willing.

2.setTotalDuration((duration) => duration - timePassed). the way to calculate total duration is changed.


This content originally appeared on DEV Community and was authored by MING


Print Share Comment Cite Upload Translate Updates
APA

MING | Sciencx (2023-03-20T21:03:59+00:00) Stopwatch – React. Retrieved from https://www.scien.cx/2023/03/20/stopwatch-react/

MLA
" » Stopwatch – React." MING | Sciencx - Monday March 20, 2023, https://www.scien.cx/2023/03/20/stopwatch-react/
HARVARD
MING | Sciencx Monday March 20, 2023 » Stopwatch – React., viewed ,<https://www.scien.cx/2023/03/20/stopwatch-react/>
VANCOUVER
MING | Sciencx - » Stopwatch – React. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2023/03/20/stopwatch-react/
CHICAGO
" » Stopwatch – React." MING | Sciencx - Accessed . https://www.scien.cx/2023/03/20/stopwatch-react/
IEEE
" » Stopwatch – React." MING | Sciencx [Online]. Available: https://www.scien.cx/2023/03/20/stopwatch-react/. [Accessed: ]
rf:citation
» Stopwatch – React | MING | Sciencx | https://www.scien.cx/2023/03/20/stopwatch-react/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.