90

I have seen lots of countdown timers in JavaScript and wanted to get one working in React.

I have borrowed this function I found online:

secondsToTime(secs){
    let hours = Math.floor(secs / (60 * 60));

    let divisor_for_minutes = secs % (60 * 60);
    let minutes = Math.floor(divisor_for_minutes / 60);

    let divisor_for_seconds = divisor_for_minutes % 60;
    let seconds = Math.ceil(divisor_for_seconds);

    let obj = {
        "h": hours,
        "m": minutes,
        "s": seconds
    };
    return obj;
  };

And then I have written this code myself

  initiateTimer = () => {
    let timeLeftVar = this.secondsToTime(60);
    this.setState({ timeLeft: timeLeftVar })
  };

  startTimer = () => {
    let interval = setInterval(this.timer, 1000);
    this.setState({ interval: interval });
  };

  timer = () => {
    if (this.state.timeLeft >0){
      this.setState({ timeLeft: this.state.timeLeft -1 });
    }
    else {
      clearInterval(this.state.interval);
      //this.postToSlack();
    }
  };

Currently onclick it will set the time on screen to: Time Remaining: 1 m : 0 s But it does not reduce it to Time Remaining: 0 m : 59 s and then Time Remaining: 0 m : 58 s etc etc

I think I need to call the function again with a different parameter. how can I go about doing this ?

Edit: I forgot to say, I would like the functionality so that I can use seconds to minutes & seconds

Anfuca
  • 1,329
  • 1
  • 14
  • 27
The worm
  • 5,580
  • 14
  • 36
  • 49
  • 1
    One of [the React documentation examples](https://facebook.github.io/react/docs/state-and-lifecycle.html) is a clock that updates itself, seems like it would be fairly useful... – T.J. Crowder Nov 30 '16 at 10:31
  • @T.J.Crowder it is semi helpful. they are just getting a time though as can return it through componentDidMount whereas I only want to extract seconds and minutes from a starting position.. – The worm Nov 30 '16 at 10:50
  • Perhaps you could put a runnable [mcve] in the question using Stack Snippets, which [support React and JSX](http://meta.stackoverflow.com/questions/338537/how-do-i-create-a-reactjs-stack-snippet-with-jsx-support), so we could see the problem in action. – T.J. Crowder Nov 30 '16 at 10:53
  • @T.J.Crowder finding it very difficult to create one in JSfiddle as I am using many components with many props across many files – The worm Nov 30 '16 at 11:04
  • @T.J.Crowder from the question, what makes sense to you? (to see if I can add more knowledge to things explained less well) – The worm Nov 30 '16 at 11:04
  • @T.J.Crowder sorry for the spam. What I need more is a way to convert seconds to seconds/minutes e.g. 10 -> 00:10 or 65 -> 01:05 in react. basically a nice way to format my state – The worm Nov 30 '16 at 11:25

20 Answers20

96

You have to setState every second with the seconds remaining (every time the interval is called). Here's an example:

class Example extends React.Component {
  constructor() {
    super();
    this.state = { time: {}, seconds: 5 };
    this.timer = 0;
    this.startTimer = this.startTimer.bind(this);
    this.countDown = this.countDown.bind(this);
  }

  secondsToTime(secs){
    let hours = Math.floor(secs / (60 * 60));

    let divisor_for_minutes = secs % (60 * 60);
    let minutes = Math.floor(divisor_for_minutes / 60);

    let divisor_for_seconds = divisor_for_minutes % 60;
    let seconds = Math.ceil(divisor_for_seconds);

    let obj = {
      "h": hours,
      "m": minutes,
      "s": seconds
    };
    return obj;
  }

  componentDidMount() {
    let timeLeftVar = this.secondsToTime(this.state.seconds);
    this.setState({ time: timeLeftVar });
  }

  startTimer() {
    if (this.timer == 0 && this.state.seconds > 0) {
      this.timer = setInterval(this.countDown, 1000);
    }
  }

  countDown() {
    // Remove one second, set state so a re-render happens.
    let seconds = this.state.seconds - 1;
    this.setState({
      time: this.secondsToTime(seconds),
      seconds: seconds,
    });
    
    // Check if we're at zero.
    if (seconds == 0) { 
      clearInterval(this.timer);
    }
  }

  render() {
    return(
      <div>
        <button onClick={this.startTimer}>Start</button>
        m: {this.state.time.m} s: {this.state.time.s}
      </div>
    );
  }
}

ReactDOM.render(<Example/>, document.getElementById('View'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="View"></div>
Brynner Ferreira
  • 1,527
  • 1
  • 21
  • 21
Fabian Schultz
  • 18,138
  • 5
  • 49
  • 56
  • 1
    this looks good.one problem is that it does not stop at 0 and goes to minus though? fix that and I'll accept it ;) – The worm Nov 30 '16 at 11:43
  • 2
    Well it's similar to what you've had in your initial code. Check if there's any seconds left and then do `clearInterval`. Updated my answer. – Fabian Schultz Nov 30 '16 at 11:51
  • 1
    You could also do a lot more optimizations, like resetting the timer, pausing, etc., but the question was targeted at how do count down and reflect that in the render. – Fabian Schultz Nov 30 '16 at 11:57
  • 1
    cheers, mine is still going into minus for some weird reason. I even console.logged(seconds) and it showed me it being 0 so will have to debug further – The worm Nov 30 '16 at 12:09
  • in fact it even got into that part but clear interval did not seem to do anything, you know why? – The worm Nov 30 '16 at 13:16
  • Is your timer object in the state? Try it like I did. – Fabian Schultz Nov 30 '16 at 13:17
  • yeh I have I think. and yes it is in the state at the top. – The worm Nov 30 '16 at 13:18
  • Try it as a global variable, not state (`this.timer = 0;`) – Fabian Schultz Nov 30 '16 at 13:20
  • globals get messy though don't they :( – The worm Nov 30 '16 at 13:22
  • found the problem! – The worm Nov 30 '16 at 13:23
  • 3
    @FabianSchultz your solution was awesome. It was really helpful for me to build my count down timer component and to get started. The code was very clean. Keep up the great work ! – Ravindra Ranwala Apr 26 '17 at 06:53
  • beware that if the component is unmounted before the timer is done a warning will be displayed saying that no `this.setState` can be called on an unmounted object. In order to avoid this you should add `componentWillUnmount(){ clearInterval(this.timer) }` to your component – Jonathan Morales Vélez Dec 27 '17 at 22:28
  • @JonathanMoralesVélez Yes, that’s very true! Left it out here for simplicity. – Fabian Schultz Dec 27 '17 at 22:29
  • @FabianSchultz setInterval may have a delay which is likely to vary per phone, I've seen this to sometimes be up to 30 seconds off. https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#Reasons_for_delays_longer_than_specified instead look at something like https://dbaron.org/log/20100309-faster-timeouts – mc. Jan 26 '18 at 16:33
  • One additional comment regarding countDown method: If you need previous state to calculate next (which is the case), you should use setState which receives a function that has previousState as argument and returns the newState: this.setState(prevState => calculateNextState(prevState)); – Arthur Rizzo May 30 '18 at 17:59
  • Trying to reset this and start timer again with a button click `resetTimer() { let timeLeftVar = this.secondsToTime(TIMER_IN_SECONDS); this.setState({ timer: 0, seconds: TIMER_IN_SECONDS, time: timeLeftVar }); this.startTimer(); }` does not seem to work – Temi 'Topsy' Bello Aug 09 '20 at 09:00
  • This will not be accurate, time will drift – epascarello Oct 06 '20 at 14:59
  • your `secondsToTime` needs to divide the seconds by anther `1000`, so it should be: `hours = Math.floor(secs / (60 * 60 * 1000))` & `divisor_for_minutes = secs % (60 * 60 * 1000)` – Biskrem Muhammad Dec 07 '21 at 20:39
65

Here is a solution using hooks, Timer component, I'm replicating same logic above with hooks

import React from 'react'
import { useState, useEffect } from 'react';

const Timer = (props:any) => {
    const {initialMinute = 0,initialSeconds = 0} = props;
    const [ minutes, setMinutes ] = useState(initialMinute);
    const [seconds, setSeconds ] =  useState(initialSeconds);
    useEffect(()=>{
    let myInterval = setInterval(() => {
            if (seconds > 0) {
                setSeconds(seconds - 1);
            }
            if (seconds === 0) {
                if (minutes === 0) {
                    clearInterval(myInterval)
                } else {
                    setMinutes(minutes - 1);
                    setSeconds(59);
                }
            } 
        }, 1000)
        return ()=> {
            clearInterval(myInterval);
          };
    });

    return (
        <div>
        { minutes === 0 && seconds === 0
            ? null
            : <h1> {minutes}:{seconds < 10 ?  `0${seconds}` : seconds}</h1> 
        }
        </div>
    )
}

export default Timer;
Masood
  • 909
  • 7
  • 11
11

Here is a simple implementation using hooks and useInterval implementation of @dan-abramov

import React, {useState, useEffect, useRef} from 'react'
import './styles.css'

const STATUS = {
  STARTED: 'Started',
  STOPPED: 'Stopped',
}

const INITIAL_COUNT = 120

export default function CountdownApp() {
  const [secondsRemaining, setSecondsRemaining] = useState(INITIAL_COUNT)
  const [status, setStatus] = useState(STATUS.STOPPED)

  const secondsToDisplay = secondsRemaining % 60
  const minutesRemaining = (secondsRemaining - secondsToDisplay) / 60
  const minutesToDisplay = minutesRemaining % 60
  const hoursToDisplay = (minutesRemaining - minutesToDisplay) / 60

  const handleStart = () => {
    setStatus(STATUS.STARTED)
  }
  const handleStop = () => {
    setStatus(STATUS.STOPPED)
  }
  const handleReset = () => {
    setStatus(STATUS.STOPPED)
    setSecondsRemaining(INITIAL_COUNT)
  }
  useInterval(
    () => {
      if (secondsRemaining > 0) {
        setSecondsRemaining(secondsRemaining - 1)
      } else {
        setStatus(STATUS.STOPPED)
      }
    },
    status === STATUS.STARTED ? 1000 : null,
    // passing null stops the interval
  )
  return (
    <div className="App">
      <h1>React Countdown Demo</h1>
      <button onClick={handleStart} type="button">
        Start
      </button>
      <button onClick={handleStop} type="button">
        Stop
      </button>
      <button onClick={handleReset} type="button">
        Reset
      </button>
      <div style={{padding: 20}}>
        {twoDigits(hoursToDisplay)}:{twoDigits(minutesToDisplay)}:
        {twoDigits(secondsToDisplay)}
      </div>
      <div>Status: {status}</div>
    </div>
  )
}

// source: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
function useInterval(callback, delay) {
  const savedCallback = useRef()

  // Remember the latest callback.
  useEffect(() => {
    savedCallback.current = callback
  }, [callback])

  // Set up the interval.
  useEffect(() => {
    function tick() {
      savedCallback.current()
    }
    if (delay !== null) {
      let id = setInterval(tick, delay)
      return () => clearInterval(id)
    }
  }, [delay])
}

// https://stackoverflow.com/a/2998874/1673761
const twoDigits = (num) => String(num).padStart(2, '0')

Here is the codesandbox implementation: https://codesandbox.io/s/react-countdown-demo-gtr4u?file=/src/App.js

abumalick
  • 2,136
  • 23
  • 27
9

Basic idea showing counting down using Date.now() instead of subtracting one which will drift over time.

class Example extends React.Component {
  constructor() {
    super();
    this.state = {
      time: {
        hours: 0,
        minutes: 0,
        seconds: 0,
        milliseconds: 0,
      },
      duration: 2 * 60 * 1000,
      timer: null
    };
    this.startTimer = this.start.bind(this);
  }

  msToTime(duration) {
    let milliseconds = parseInt((duration % 1000));
    let seconds = Math.floor((duration / 1000) % 60);
    let minutes = Math.floor((duration / (1000 * 60)) % 60);
    let hours = Math.floor((duration / (1000 * 60 * 60)) % 24);

    hours = hours.toString().padStart(2, '0');
    minutes = minutes.toString().padStart(2, '0');
    seconds = seconds.toString().padStart(2, '0');
    milliseconds = milliseconds.toString().padStart(3, '0');

    return {
      hours,
      minutes,
      seconds,
      milliseconds
    };
  }

  componentDidMount() {}

  start() {
    if (!this.state.timer) {
      this.state.startTime = Date.now();
      this.timer = window.setInterval(() => this.run(), 10);
    }
  }

  run() {
    const diff = Date.now() - this.state.startTime;
    
    // If you want to count up
    // this.setState(() => ({
    //  time: this.msToTime(diff)
    // }));
    
    // count down
    let remaining = this.state.duration - diff;
    if (remaining < 0) {
      remaining = 0;
    }
    this.setState(() => ({
      time: this.msToTime(remaining)
    }));
    if (remaining === 0) {
      window.clearTimeout(this.timer);
      this.timer = null;
    }
  }

  render() {
    return ( <
      div >
      <
      button onClick = {
        this.startTimer
      } > Start < /button> {
        this.state.time.hours
      }: {
        this.state.time.minutes
      }: {
        this.state.time.seconds
      }. {
        this.state.time.milliseconds
      }:
      <
      /div>
    );
  }
}

ReactDOM.render( < Example / > , document.getElementById('View'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="View"></div>
epascarello
  • 204,599
  • 20
  • 195
  • 236
  • This is a much more stable method. Problem however is, that pause and continue is a bit more challenging. – Tosh Mar 15 '21 at 13:07
  • @Tosh not really.... you know how much time elapsed when it is pasued, you store that. On continue you figure out the difference and set a new start time. – epascarello Mar 15 '21 at 13:18
7

simple resolution:

import React, { useState, useEffect } from "react";

const Timer = ({ delayResend = "180" }) => {
  const [delay, setDelay] = useState(+delayResend);
  const minutes = Math.floor(delay / 60);
  const seconds = Math.floor(delay % 60);
  useEffect(() => {
    const timer = setInterval(() => {
      setDelay(delay - 1);
    }, 1000);

    if (delay === 0) {
      clearInterval(timer);
    }

    return () => {
      clearInterval(timer);
    };
  });

  return (
    <>
      <span>
        {minutes}:{seconds}
      </span>
    </>
  );
};

export default Timer;
3

The problem is in your "this" value. Timer function cannot access the "state" prop because run in a different context. I suggest you to do something like this:

...
startTimer = () => {
  let interval = setInterval(this.timer.bind(this), 1000);
  this.setState({ interval });
};

As you can see I've added a "bind" method to your timer function. This allows the timer, when called, to access the same "this" of your react component (This is the primary problem/improvement when working with javascript in general).

Another option is to use another arrow function:

startTimer = () => {
  let interval = setInterval(() => this.timer(), 1000);
  this.setState({ interval });
};
Vaibhav Mule
  • 5,016
  • 4
  • 35
  • 52
Roberto Conte Rosito
  • 2,080
  • 12
  • 22
3

Countdown of user input

Interface Screenshot screenshot

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

class App extends Component {
  constructor() {
    super();
    this.state = {
      hours: 0,
      minutes: 0,
      seconds:0
    }
    this.hoursInput = React.createRef();
    this.minutesInput= React.createRef();
    this.secondsInput = React.createRef();
  }

  inputHandler = (e) => {
    this.setState({[e.target.name]: e.target.value});
  }

  convertToSeconds = ( hours, minutes,seconds) => {
    return seconds + minutes * 60 + hours * 60 * 60;
  }

  startTimer = () => {
    this.timer = setInterval(this.countDown, 1000);
  }

  countDown = () => {
    const  { hours, minutes, seconds } = this.state;
    let c_seconds = this.convertToSeconds(hours, minutes, seconds);

    if(c_seconds) {

      // seconds change
      seconds ? this.setState({seconds: seconds-1}) : this.setState({seconds: 59});

      // minutes change
      if(c_seconds % 60 === 0 && minutes) {
        this.setState({minutes: minutes -1});
      }

      // when only hours entered
      if(!minutes && hours) {
        this.setState({minutes: 59});
      }

      // hours change
      if(c_seconds % 3600 === 0 && hours) {
        this.setState({hours: hours-1});
      }

    } else {
      clearInterval(this.timer);
    }
  }


  stopTimer = () => {
    clearInterval(this.timer);
  }

  resetTimer = () => {
    this.setState({
      hours: 0,
      minutes: 0,
      seconds: 0
    });
    this.hoursInput.current.value = 0;
    this.minutesInput.current.value = 0;
    this.secondsInput.current.value = 0;
  }


  render() {
    const { hours, minutes, seconds } = this.state;

    return (
      <div className="App">
         <h1 className="title"> (( React Countdown )) </h1>
         <div className="inputGroup">
            <h3>Hrs</h3>
            <input ref={this.hoursInput} type="number" placeholder={0}  name="hours"  onChange={this.inputHandler} />
            <h3>Min</h3>
            <input  ref={this.minutesInput} type="number"  placeholder={0}   name="minutes"  onChange={this.inputHandler} />
            <h3>Sec</h3>
            <input   ref={this.secondsInput} type="number"  placeholder={0}  name="seconds"  onChange={this.inputHandler} />
         </div>
         <div>
            <button onClick={this.startTimer} className="start">start</button>
            <button onClick={this.stopTimer}  className="stop">stop</button>
            <button onClick={this.resetTimer}  className="reset">reset</button>
         </div>
         <h1> Timer {hours}: {minutes} : {seconds} </h1>
      </div>

    );
  }
}

export default App;

3

I had the same problem and I found this npm package for a countdown.

  1. install the package

    npm install react-countdown --save or

    yarn add react-countdown

  2. import the package to your file

    import Countdown from 'react-countdown';

  3. call the imported "Countdown" inside a render method and pass a date

    <Countdown date={new Date('2021-09-26T10:05:29.896Z').getTime()}> or

    <Countdown date={new Date("Sat Sep 26 2021")}>

Here is an example for you.

import React from "react";
import ReactDOM from "react-dom";
import Countdown from "react-countdown";

// Random component
const Completionist = () => <span>You are good to go!</span>;

ReactDOM.render(
  <Countdown date={new Date('2021-09-26T10:05:29.896Z').getTime()}>
    <Completionist />
  </Countdown>,
  document.getElementById("root")
);

you can see the detailed document here https://www.npmjs.com/package/react-countdown

Kasun Shanaka
  • 725
  • 1
  • 8
  • 14
1

functionality : 1)Start 2)Reset

functional component

import {useState, useCallback} from 'react';
const defaultCount = 10;
const intervalGap = 300;

const Counter = () => {
    const [timerCount, setTimerCount] = useState(defaultCount);
    
    const startTimerWrapper = useCallback((func)=>{
        let timeInterval: NodeJS.Timer;
        return () => {
            if(timeInterval) {
                clearInterval(timeInterval)
            }
            setTimerCount(defaultCount)
            timeInterval = setInterval(() => {
                func(timeInterval)
            }, intervalGap)
        }
    }, [])

    const timer = useCallback(startTimerWrapper((intervalfn: NodeJS.Timeout) => {
         setTimerCount((val) => {
            if(val === 0 ) {
                clearInterval(intervalfn);
                return val
            } 
            return val - 1
        })
    }), [])

    return <>
        <div> Counter App</div>
        <div> <button onClick={timer}>Start/Reset</button></div>
        <div> {timerCount}</div>
    </>
}
export default Counter;
1

When you are using functional components the above code is a good option to do it:

import React, { useState, useEffect } from "react";
import { MessageStrip } from "@ui5/webcomponents-react";
import "./Timer.scss";

const nMinuteSeconds = 60;
const nSecondInMiliseconds = 1000;

const convertMinutesToMiliseconds = (minute) =>
  minute * nMinuteSeconds * nSecondInMiliseconds;

const convertMilisecondsToHour = (miliseconds) => new Date(miliseconds).toISOString().slice(11, -5);

export default function Counter({ minutes, onTimeOut }) {
  let [timerCount, setTimerCount] = useState(
    convertMinutesToMiliseconds(minutes)
  );
  let interval;

  useEffect(() => {
    if (interval) {
      clearInterval(interval);
    }

    interval = setInterval(() => {
      if (timerCount === 0 && interval) {
        onTimeOut();
        clearInterval(interval);
      }

      setTimerCount((timerCount -= nSecondInMiliseconds));
    }, nSecondInMiliseconds);
  }, []);

  return (
    <>
      <MessageStrip design="Information" hide-close-button>
        Time left: {convertMilisecondsToHour(timerCount)}
      </MessageStrip>
    </>
  );
}
M.Georgiev
  • 43
  • 6
1

Here's a simple implementation using a custom hook:

import * as React from 'react';

// future time from where countdown will start decreasing
const futureTime = new Date( Date.now() + 5000 ).getTime(); // adding 5 seconds

export const useCountDown = (stop = false) => {

   const [time, setTime] = React.useState(
       futureTime - Date.now()
   );

   // ref to store interval which we can clear out later
   // when stop changes through parent component (component that uses this hook)
   // causing the useEffect callback to trigger again
   const intervalRef = React.useRef<NodeJS.Timer | null>(null);

   React.useEffect(() => {
       if (intervalRef.current) {
           clearInterval(intervalRef.current);
           intervalRef.current = null;
           return;
       }

       const interval = intervalRef.current = setInterval(() => {
          setTime(futureTime - Date.now());
       }, 1000);

      return () => clearInterval(interval);
   }, [stop]);

   return getReturnValues(time);
};

const getReturnValues = (countDown: number) => {
    const days = Math.floor(countDown / (1000 * 60 * 60 * 24));
    const hours = Math.floor(
      (countDown % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
    );
    const minutes = Math.floor((countDown % (1000 * 60 * 60)) / (1000 * 60));
    const seconds = Math.floor((countDown % (1000 * 60)) / 1000);

    return [days, hours, minutes, seconds];
};

Example of using this hook:

function App() {

    const [timerStopped, stopTimer] = React.useState(false);
    const [,hours,minutes,seconds]  = useCountDown(timerStopped);

   // to stop the interval
   if( !timerStopped && minutes + seconds <= 0 ) {
      stopTimer(true);
   }

   return (
     <div className="App">
       Time Left: {hours}:{minutes}:{seconds}
       { timerStopped ? (
           <h1>Time's up</h1>
        ) : null }
     </div>
   );
}
Sumit Wadhwa
  • 2,825
  • 1
  • 20
  • 34
1

A simple 24-hour countdown that can easily be customized to fit different scenarios

setInterval(function time() {
let d = new Date();
let hours = 24 - d.getHours();
let min = 60 - d.getMinutes();
if ((min + "").length == 1) {
  min = "0" + min;
}
let sec = 60 - d.getSeconds();
if ((sec + "").length == 1) {
  sec = "0" + sec;
}

setState(hours + ":" + min + ":" + sec);

  }, 1000);
Amer NM
  • 159
  • 2
  • 7
1

Here is my solution React + typescript:

When it comes to JavaScript timers, it's important to note that they are not always guaranteed to be perfectly precise. The accuracy of timers can vary depending on various factors, including the performance of the underlying system and the load on the browser.

Instead of relying on setInterval with a fixed interval of 1000 milliseconds, we can use the performance.now() method to calculate the actual time elapsed between each tick. This allows to account for any delay introduced by the execution of other code.

By using a smaller interval (e.g., 10 milliseconds), we increase the frequency of checks for more precision.

interface ICountdownTimerProps {
    minutes: number
}

export const CountdownTimer = ({ minutes }: ICountdownTimerProps) => {
    let startTimestamp = performance.now()
    const secondBase = 1000
    const minuteBase = 60 * secondBase

    const [timeLeft, setTimeLeft] = useState(minutes * minuteBase)

    const secondTick = () => {
        setTimeLeft((prevTimeLeft) => {
            const timeLeftAfterTick = prevTimeLeft - secondBase
            if (timeLeftAfterTick < 0) {
                return prevTimeLeft
            }
            return timeLeftAfterTick
        })
    }

    const formatTime = (time: number, type: "seconds" | "minutes") => {
        switch (type) {
            case "seconds":
            case "minutes":
                return time.toString().padStart(2, "0")
            default:
                return ""
        }
    }

    const getMinutes = (timeLeft: number) => {
        return formatTime(Math.floor(timeLeft / minuteBase), "minutes")
    }

    const getSeconds = (timeLeft: number) => {
        return formatTime(Math.floor((timeLeft % minuteBase) / secondBase), "seconds")
    }

    useEffect(() => {
        const interval = setInterval(() => {
            const currentTimestamp = performance.now()
            const elapsed = currentTimestamp - startTimestamp

            if (elapsed >= secondBase) {
                startTimestamp = currentTimestamp
                secondTick()
            }
        }, 10)

        return () => {
            clearInterval(interval)
        }
    }, [])

    return (
        <div className="countdown-timer">
            <span>{getMinutes(timeLeft)}</span>:<span>{getSeconds(timeLeft)}</span>
        </div>
    )
}
0

The one downside with setInterval is that it can slow down the main thread. You can do a countdown timer using requestAnimationFrame instead to prevent this. For example, this is my generic countdown timer component:

class Timer extends Component {
  constructor(props) {
    super(props)
    // here, getTimeRemaining is a helper function that returns an 
    // object with { total, seconds, minutes, hours, days }
    this.state = { timeLeft: getTimeRemaining(props.expiresAt) }
  }

  // Wait until the component has mounted to start the animation frame
  componentDidMount() {
    this.start()
  }

  // Clean up by cancelling any animation frame previously scheduled
  componentWillUnmount() {
    this.stop()
  }

  start = () => {
    this.frameId = requestAnimationFrame(this.tick)
  }

  tick = () => {
    const timeLeft = getTimeRemaining(this.props.expiresAt)
    if (timeLeft.total <= 0) {
      this.stop()
      // ...any other actions to do on expiration
    } else {
      this.setState(
        { timeLeft },
        () => this.frameId = requestAnimationFrame(this.tick)
      )
    }
  }

  stop = () => {
    cancelAnimationFrame(this.frameId)
  }

  render() {...}
}
Sia
  • 8,894
  • 5
  • 31
  • 50
  • 1
    Nice! But I think you could optimize by preventing too many render. You don't have to `setState` (and rerender) every frame (~30 per sec). You could `setState` only if `timeLeft` (in seconds) changes. And maybe use `shouldComponentUpdate` ? Am I right ? – TeChn4K Jul 20 '18 at 13:32
0

Here is a TypeScript version of CountDown Timer in React. I used code of brother Masood and M.Georgiev

import React, {useState, useEffect, useCallback} from "react";

const Minute_to_Seconds = 60;
const Seconds_to_milliseconds = 1000;

export interface CounterProps {
    minutes:number,
    statusAlert: (status: string)=>void,
}


export interface TimerProps {

    initialMinute: number,
    initialSeconds: number,
}

const Counter: React.FC<CounterProps> = (props) => {

    const convert_Minutes_To_MiliSeconds = (minute:number) => {

        return  minute * Minute_to_Seconds * Seconds_to_milliseconds;
    }

    const convert_Mili_Seconds_To_Hour = (miliseconds:number) => {

        return new Date(miliseconds).toISOString().slice(11, -5);
    }

    const convert_Mili_Seconds_To_Minute = (miliseconds:number) => {

        return new Date(miliseconds).toISOString().slice(11, -5);
    }

    const [timer_State, setTimer_State]=useState(0);

    const [timerCount, setTimerCount] = useState(convert_Minutes_To_MiliSeconds(props.minutes));

    useEffect(() => {


        if (timerCount > 0) {

            const interval = setInterval(() => {

                    if (timer_State === 0) {

                        props.statusAlert("start");
                        setTimer_State(1);
                    }


                    let tempTimerCount = timerCount;
                    tempTimerCount -= Seconds_to_milliseconds;
                    setTimerCount(tempTimerCount);
                },
                (timer_State === 0)
                    ? 0

                    : Seconds_to_milliseconds

            );
            return () => {


                clearInterval(interval);
            }


        }
        else{

            props.statusAlert("end");
        }



    }, [

        timer_State,
        timerCount,
        props,
    ]);

    return (
        <p>
            Time left: {convert_Mili_Seconds_To_Hour(timerCount)}
        </p>
    );
}



const Timer: React.FC<TimerProps> = (props) => {

    const [ minutes, setMinutes ] = useState(props.initialMinute);
    const [seconds, setSeconds ] =  useState(props.initialSeconds);

    useEffect(()=>{
        const myInterval = setInterval(() => {
            if (seconds > 0) {
                setSeconds(seconds - 1);
            }
            if (seconds === 0) {
                if (minutes === 0) {
                    clearInterval(myInterval)
                } else {
                    setMinutes(minutes - 1);
                    setSeconds(59);
                }
            }
        }, 1000)
        return ()=> {
            clearInterval(myInterval);
        };
    });

    return (
        <div>
            { ((minutes === 0) && (seconds === 0))
                ? "Press F5 to Refresh"
                : <h1> {minutes}:{seconds < 10 ?  `0${seconds}` : seconds}</h1>
            }
        </div>
    )
}


const RCTAPP=()=> {

    const status_Alert2=(status: string)=> {

        console.log("__________________________==================== status: ", status);
        if (status==="start"){
            alert("Timer started");
        }
        else{
            alert("Time's up");
        }
    }

    return (
        <div style={{textAlign: "center"}}>

            <Counter
                minutes={1}
                // minutes={0.1}
                statusAlert={status_Alert2}
            />

            <Timer
                initialMinute={0}
                initialSeconds={30}
            />

        </div>

    );
}


export default RCTAPP;
ArefinDe
  • 678
  • 7
  • 13
0

Typescript/Hooks/Shorter version of @Masood's answer

import { useState, useEffect } from 'react';

type Props = {
  initMin: number,
  initSecs: number
};

const Timer = ({initMins, initSecs}: Props) => {
    // Combining useState values together for brevity
    const [ [mins, secs], setCountdown ] = useState([initMins, initSecs]);

  /**
   * Triggers each second and whenever mins/seconds updates itself. 
   */
  useEffect(() => {
    // Timer that decrements itself each second and updates the mins/seconds downwards
    let timerInterval = setInterval(() => {
      // Countdown timer up, clear timer and do nothing
      if (mins === 0 && secs === 0) {
        clearInterval(timerInterval);
      } else if (secs === 0) {
        // Might be correct to set seconds to 59, but not sure
        // should decrement from 60 seconds yeah? 
        setCountdown([mins - 1, 60]);
      } else {
        setCountdown([mins, secs - 1]);
      }
    }, 1000);

    return () => {
      clearInterval(timerInterval);
    };
  }, [mins, secs]);

    return (
        <div>
        { mins === 0 && secs === 0
            ? null
            : <h1> {mins}:{secs < 10 ?  `0${secs}` : secs}</h1> 
        }
        </div>
    )
}

export default Timer;
wanna_coder101
  • 508
  • 5
  • 19
0

As we don't want the timer at the highest priority than other states so we will use useTransition hook. delay is the time in seconds 180s = 3min.

import React, { useState, useEffect, useTransition } from "react";

const Timer = ({ delayResend = "180" }) => {
  const [delay, setDelay] = useState(+delayResend);
  const [minutes, setMinutes] = useState(0);
  const [seconds, setSeconds] = useState(0);

  const [isPending, startTransition] = useTransition();

  useEffect(() => {
    const timer = setInterval(() => {
      startTransition(() => {
       setDelay(delay - 1);
       setMinutes(Math.floor(delay / 60));
       setSeconds(Math.floor(delay % 60));
   
     });
     
    }, 1000);

    if (delay === 0) {
      clearInterval(timer);
    }

    return () => {
      clearInterval(timer);
    };
  });

  return (
    <>
      <span>
        {minutes}:{seconds}
      </span>
    </>
  );
};

export default Timer;
Sehrish Waheed
  • 1,230
  • 14
  • 17
0
import { useEffect, useMemo, useState } from "react";

const SECOND = 1000;
const MINUTE = SECOND * 60;
const HOUR = MINUTE * 60;
const DAY = HOUR * 24;

export const Timer = ({ deadline = new Date().toString() }) => {
    const parsedDeadline = useMemo(() => Date.parse(deadline), [deadline]);
    const [time, setTime] = useState(parsedDeadline - Date.now());

    useEffect(() => {
        const interval = setInterval(
            () => setTime(parsedDeadline - Date.now()),
            1000,
        );

        return () => clearInterval(interval);
    }, [parsedDeadline]);

    return (
        <div className="timer">
            {Object.entries({
                Days: time / DAY,
                Hours: (time / HOUR) % 24,
                Minutes: (time / MINUTE) % 60,
                Seconds: (time / SECOND) % 60,
            }).map(([label, value]) => (
                <div key={label} className="col-4">
                    <div className="box">
                        <p>{`${Math.floor(value)}`.padStart(2, "0")}</p>
                        <span className="text">{label}</span>
                    </div>
                </div>import { useEffect, useMemo, useState } from "react";

const SECOND = 1000;
const MINUTE = SECOND * 60;
const HOUR = MINUTE * 60;
const DAY = HOUR * 24;

export const Timer = ({ deadline = new Date().toString() }) => {
    const parsedDeadline = useMemo(() => Date.parse(deadline), [deadline]);
    const [time, setTime] = useState(parsedDeadline - Date.now());

    useEffect(() => {
        const interval = setInterval(
            () => setTime(parsedDeadline - Date.now()),
            1000,
        );

        return () => clearInterval(interval);
    }, [parsedDeadline]);

    return (
        <div className="timer">
            {Object.entries({
                Days: time / DAY,
                Hours: (time / HOUR) % 24,
                Minutes: (time / MINUTE) % 60,
                Seconds: (time / SECOND) % 60,
            }).map(([label, value]) => (
                <div key={label} className="col-4">
                    <div className="box">
                        <p>{`${Math.floor(value)}`.padStart(2, "0")}</p>
                        <span className="text">{label}</span>
                    </div>
                </div>
            ))}
        </div>
    );
};
            ))}
        </div>
    );
};
0

Usually there is no need for extreme precision so you can use setTimeout, but if you need some finesse with time handling you can change useEffect's callback, just have to set timer to needed value. Still, it's a component rerender every second or less.

const {useEffect, useState} = React;
const Countdown = (props) => {
    const [timer, setTimer] = useState(120); //in seconds
    const timerToString = () => {
      let hours = ('0' + Math.floor(timer/3600)).slice(-2);
      let minutes = ('0' + Math.floor(timer/60)).slice(-2);
      let seconds = ('0' + timer%60).slice(-2);
      return /*hours + ":" +*/ minutes + ":" + seconds;
    }
   
    useEffect(()=>{
      if(timer > 0){
        setTimeout(()=>{
          setTimer(timer-1);
        }, 1000)
      }
    }, [timer]);
   
    
    return (
      <div> {timerToString()}</div>
    )
}

ReactDOM.createRoot(
    document.getElementById("root")
).render(
    <Countdown />
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
Bedivierre
  • 43
  • 5
0
> useEffect(() => {
>     const timer = setInterval(() => {
>       if (countdown > 1) {
>         setCountdown(countdown - 1);
>       } else {
>         clearInterval(timer);
>       }
>     }, 1000);
> 
>     return () => {
>       clearInterval(timer);
>     };   }, [countdown]);
  • This does not provide an answer to the question. Once you have sufficient [reputation](https://stackoverflow.com/help/whats-reputation) you will be able to [comment on any post](https://stackoverflow.com/help/privileges/comment); instead, [provide answers that don't require clarification from the asker](https://meta.stackexchange.com/questions/214173/why-do-i-need-50-reputation-to-comment-what-can-i-do-instead). - [From Review](/review/late-answers/34775564) – Dhaifallah Aug 04 '23 at 13:25