The worm
The worm

Reputation: 5888

Countdown timer in React

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

Upvotes: 95

Views: 371574

Answers (20)

> useEffect(() => {
>     const timer = setInterval(() => {
>       if (countdown > 1) {
>         setCountdown(countdown - 1);
>       } else {
>         clearInterval(timer);
>       }
>     }, 1000);
> 
>     return () => {
>       clearInterval(timer);
>     };   }, [countdown]);

Upvotes: 1

Bedivierre
Bedivierre

Reputation: 71

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>

Upvotes: 2

Igor Podosonov
Igor Podosonov

Reputation: 11

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>
    )
}

Upvotes: 1

Anonymus S
Anonymus S

Reputation: 1

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>
    );
};

Upvotes: 0

Sehrish Waheed
Sehrish Waheed

Reputation: 1555

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;

Upvotes: 0

Amer NM
Amer NM

Reputation: 179

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);

Upvotes: 1

wanna_coder101
wanna_coder101

Reputation: 206

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;

Upvotes: 0

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;

Upvotes: 9

Sumit Wadhwa
Sumit Wadhwa

Reputation: 3217

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>
   );
}

Upvotes: 1

ArefinDe
ArefinDe

Reputation: 678

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;

Upvotes: 0

Kasun Shanaka
Kasun Shanaka

Reputation: 833

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

Upvotes: 3

M.Georgiev
M.Georgiev

Reputation: 43

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>
    </>
  );
}

Upvotes: 1

Saurav Singh
Saurav Singh

Reputation: 31

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;

Upvotes: 1

abumalick
abumalick

Reputation: 2346

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

Upvotes: 14

user3869471
user3869471

Reputation: 41

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;

Upvotes: 4

epascarello
epascarello

Reputation: 207511

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>

Upvotes: 10

Masood
Masood

Reputation: 1019

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;

Upvotes: 74

Fabian Schultz
Fabian Schultz

Reputation: 18546

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>

Upvotes: 100

Sia
Sia

Reputation: 9212

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() {...}
}

Upvotes: 0

Roberto Conte Rosito
Roberto Conte Rosito

Reputation: 2108

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 });
};

Upvotes: 3

Related Questions