Im trying to render a count down timer on screen with react hooks, but I'm not sure the best way to render it.
I know i'm supposed to use the useEffect to compare current state to previous state, but I don't think I'm doing it correctly.
I would appreciate the help!
I've tried a couple of different ways, none of them work, like setting state whenever whenever it updates, but it just ends up flickering like crazy.
const Timer = ({ seconds }) => {
const [timeLeft, setTimeLeft] = useState('');
const now = Date.now();
const then = now + seconds * 1000;
const countDown = setInterval(() => {
const secondsLeft = Math.round((then - Date.now()) / 1000);
if(secondsLeft <= 0) {
clearInterval(countDown);
console.log('done!');
return;
}
displayTimeLeft(secondsLeft);
}, 1000);
const displayTimeLeft = seconds => {
let minutesLeft = Math.floor(seconds/60) ;
let secondsLeft = seconds % 60;
minutesLeft = minutesLeft.toString().length === 1 ? "0" + minutesLeft : minutesLeft;
secondsLeft = secondsLeft.toString().length === 1 ? "0" + secondsLeft : secondsLeft;
return `${minutesLeft}:${secondsLeft}`;
}
useEffect(() => {
setInterval(() => {
setTimeLeft(displayTimeLeft(seconds));
}, 1000);
}, [seconds])
return (
<div><h1>{timeLeft}</h1></div>
)
}
export default Timer;```
const Timer = ({ seconds }) => {
// initialize timeLeft with the seconds prop
const [timeLeft, setTimeLeft] = useState(seconds);
useEffect(() => {
// exit early when we reach 0
if (!timeLeft) return;
// save intervalId to clear the interval when the
// component re-renders
const intervalId = setInterval(() => {
setTimeLeft(timeLeft - 1);
}, 1000);
// clear interval on re-render to avoid memory leaks
return () => clearInterval(intervalId);
// add timeLeft as a dependency to re-rerun the effect
// when we update it
}, [timeLeft]);
return (
<div>
<h1>{timeLeft}</h1>
</div>
);
};
You should use setInterval. I just wanted to add a slight improvement over #Asaf solution. You do not have to reset the interval every time you change the value. It's gonna remove the interval and add a new one every time (Might as well use a setTimeout in that case). So you can remove the dependencies of your useEffect (i.e. []):
function Countdown({ seconds }) {
const [timeLeft, setTimeLeft] = useState(seconds);
useEffect(() => {
const intervalId = setInterval(() => {
setTimeLeft((t) => t - 1);
}, 1000);
return () => clearInterval(intervalId);
}, []);
return <div>{timeLeft}s</div>;
}
Working example:
Note in the setter, we need to use this syntax (t) => t - 1 so that we get the latest value each time (see: https://reactjs.org/docs/hooks-reference.html#functional-updates).
Edit (22/10/2021)
If you want to use a setInterval and stop the counter at 0, here is what you can do:
function Countdown({ seconds }) {
const [timeLeft, setTimeLeft] = useState(seconds);
const intervalRef = useRef(); // Add a ref to store the interval id
useEffect(() => {
intervalRef.current = setInterval(() => {
setTimeLeft((t) => t - 1);
}, 1000);
return () => clearInterval(intervalRef.current);
}, []);
// Add a listener to `timeLeft`
useEffect(() => {
if (timeLeft <= 0) {
clearInterval(intervalRef.current);
}
}, [timeLeft]);
return <div>{timeLeft}s</div>;
}
Here's another alternative with setTimeout
const useCountDown = (start) => {
const [counter, setCounter] = useState(start);
useEffect(() => {
if (counter === 0) {
return;
}
setTimeout(() => {
setCounter(counter - 1);
}, 1000);
}, [counter]);
return counter;
};
Example
Here's my version of a hook, with a "stop" countdown.
Also, I added a "fps" (frames p/sec), to show the countdown with decimal places!
import { useEffect, useRef, useState } from 'react'
interface ITimer {
timer: number
startTimer: (time: number) => void
stopTimer: () => void
}
interface IProps {
start?: number
fps?: number
}
const useCountDown = ({ start, fps }: IProps): ITimer => {
const [timer, setTimer] = useState(start || 0)
const intervalRef = useRef<NodeJS.Timer>()
const stopTimer = () => {
if (intervalRef.current) clearInterval(intervalRef.current)
}
const startTimer = (time: number) => {
setTimer(time)
}
useEffect(() => {
if (timer <= 0) return stopTimer()
intervalRef.current = setInterval(() => {
setTimer((t) => t - 1 / (fps || 1))
}, 1000 / (fps || 1))
return () => {
if (intervalRef.current) clearInterval(intervalRef.current)
}
}, [timer])
return { timer, startTimer, stopTimer }
}
export default useCountDown
Here is a small component - CountdownTimer - accepting an input parameter expiresIn representing the time left in seconds.
We use useState to define min and sec which we display on the screen, and also we use timeLeft to keep track of the time that's left.
We use useEffect to decrement timeLeft and recalculate min and sec every second.
Also, we use formatTime to format the minutes and seconds before displaying them on the screen. If minutes and seconds are both equal to zero we stop the countdown timer.
import { useState, useEffect } from 'react';
const CountdownTimer = ({expiresIn}) => {
const [min, setMin] = useState(0);
const [sec, setSec] = useState(0);
const [timeLeft, setTimeLeft] = useState(expiresIn);
const formatTime = (t) => t < 10 ? '0' + t : t;
useEffect(() => {
const interval = setInterval(() => {
const m = Math.floor(timeLeft / 60);
const s = timeLeft - m * 60;
setMin(m);
setSec(s);
if (m <= 0 && s <= 0) return () => clearInterval(interval);
setTimeLeft((t) => t - 1);
}, 1000);
return () => clearInterval(interval);
}, [timeLeft]);
return (
<>
<span>{formatTime(min)}</span> : <span>{formatTime(sec)}</span>
</>
);
}
export default CountdownTimer;
Optionally we can pass a setter setIsTerminated to trigger an event in the parent component once the countdown is completed.
const CountdownTimer = ({expiresIn, setIsTerminated = null}) => {
...
For example, we can trigger it when minutes and seconds are both equal to zero:
if (m <= 0 && s <= 0) {
if (setTerminated) setIsTerminated(true);
return () => clearInterval(interval);
}
Related
setTimeout function works in line with my other setTimeout function as long as I keep the window open. However, if I go to another tab/window and then come back to the timer, it will be out of sync with the other countdown.
This is the first timer:
const [isDisabled, setIsDisabled] = useState(false);
const handleOnClickUp = () => {
setIsDisabled(true);
setTimeout(() => setIsDisabled(false), 60000);}
Second Timer:
const [counter, setCounter] = useState(60);
if (isDisabled == true) {
setTimeout(() => setCounter(counter - 1), 1000);
}
if (counter == 0) {
setCounter(60);
}
Returning:
return (
<>
<div>{counter}</div>
<button
disabled={isDisabled}
onClick={handleOnClickUp}
className="bg-gray-500"
>
Up
</button>
</>
);
TLDR: How can I sync the two timers together with no problems?
Chrome and most other browsers have low-priority execution when a tab is inactive, i.e., setCounter is not being called as frequently. I faced a similar issue a while back and then shifted to using requestAnimationFrame.
Here's the example code from React:
const countDown = 30 * 1000;
const defaultTargetDate = new Date().getTime();
const [targetDate, setTargetDate] = useState(new Date(defaultTargetDate));
const [remainingSeconds, setRemainingSeconds] = useState(countDown / 1000);
const countItDown = () =>
requestAnimationFrame(() => {
const diff = Math.floor((targetDate - new Date().getTime()) / 1000);
setRemainingSeconds(diff);
if (diff > 0) {
countItDown();
}
});
useEffect(() => {
countItDown();
}, [targetDate]);
I want to make a countdown timer with start, stop, resume and reset buttons. However, I could not figure out why my code does not work. I am guessing that the issue lies in the part on timer, setInterval and clearInterval.
I have attached snippets of my code below.
Help would be greatly appreciated, thanks!
function Countdown() {
const [ timerRunning, setTimerRunning ] = useState(false);
const [ startTime, setStartTime ] = useState(0);
const [ totalTime, setTotalTime ] = useState(0);
var timer;
const startTimer = () => {
setTimerRunning(true);
setStartTime(totalTime);
setTotalTime(totalTime);
timer = setInterval(() => {
const remainingTime = totalTime - 1000;
if (remainingTime >= 0) {
setTotalTime(remainingTime);
} else {
clearInterval(timer);
setTimerRunning(false);
setStartTime(0);
setTotalTime(0);
}
}, 1000);
};
const stopTimer = () => {
clearInterval(timer);
setTimerRunning(false);
};
you have to put your setInterval in React.useEffect take a look at this post
some suggestions for your code:
use className instead of class attribute in your elements.
don't use var or let in react functional components.
useState doesn't change value immediately. So it's value is not actually changing. To avoid this use UseEffect hook.
useEffect(() => {
let intervalid
if (timerRunning){
intervalid = setInterval(() => {
const remainingTime = totalTime - 1000;
if (remainingTime >= 0) {
setTotalTime(remainingTime);
} else {
clearInterval(intervalid);
setTimerRunning(false);
setStartTime(0);
setTotalTime(0);
}
}, 1000);
}
return () => {
clearInterval(intervalid)
}
}, [timerRunning,totalTime])
My timer in Reactjs is not working correctly with a delay...
I noticed that over time, my timer starts to lag if I switch to another browser tab or window. Why is this happening and how to fix it?
import React, { Component } from 'react';
class Test extends Component {
constructor(props) {
super(props);
this.state = {
counter: 0
}
}
componentDidMount() {
this.timer = setInterval(() => {
this.setState((prevState) => ({ counter: prevState.counter > 0 ? prevState.counter - 1 : 0 }));
}, 1000);
}
componentDidUpdate() {
var target_date = new Date();
target_date.setHours(23,59,59);
var current_date = new Date();
current_date.getHours();
var counter = (target_date.getTime() - current_date.getTime()) / 1000;
if (this.state.counter === 0) {
this.setState({ counter: counter });
}
}
componentWillUnmount() {
clearInterval(this.timer);
}
render() {
const padTime = time => {
return String(time).length === 1 ? `0${time}` : `${time}`;
};
const format = time => {
const hours = Math.floor(time / (60 * 60));
const minutes = Math.floor(time % (60 * 60) / 60);
const seconds = time % 60;
return `${padTime(hours)}:${padTime(minutes)}:${padTime(seconds)}`;
};
return (
<div>{format(this.state.counter)}</div>
);
}
}
export default Test;
Please help me!
setTimeout() and setInterval() don't always call the callback function on time. It can be called later if JavaScript is doing other stuff, and/or might be called less frequent if the tab is not focused.
The solution is fairly simple, instead of using a counter to keep track of the seconds past. You should store a reference point start. Then when rendering you can calculate counter by comparing start with the current time.
Here is an example using an interval of 1ms to better display the difference.
const { useState, useEffect } = React;
// Does NOT correctly keep track of time.
function TimerA() {
const [counter, setCounter] = useState(0);
useEffect(() => {
const intervalID = setInterval(() => setCounter(counter => counter + 1), 1);
return () => clearInterval(intervalID);
}, []);
return (
<div>
I will lag behind at some point.<br />
Milliseconds passed: {counter}
</div>
);
}
// Does correctly keep track of time.
function TimerB() {
const [start] = useState(Date.now());
const [now, setNow] = useState(start);
const counter = now - start;
useEffect(() => {
const intervalID = setInterval(() => setNow(Date.now()), 1);
return () => clearInterval(intervalID);
}, []);
return (
<div>
I will always stay on time.<br />
Milliseconds passed: {counter}
</div>
);
}
ReactDOM.render(
<div>
<TimerA />
<hr />
<TimerB />
</div>,
document.querySelector("#root")
);
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="root"></div>
Don't use 1ms in production code. If the callback takes more time to execute than the interval (which it probably does in the above snippet). Then the callback queue will only grow until eventually something gives.
If you are using class components you don't have to store now in a state. Instead it could be a normal variable const now = Date.now() and you would use setInterval(() => this.forceUpdate(), ...). The only reason I store now in a state is because functional components don't have access to forceUpdate().
Now that you hopefully understand the issue and the solution, we can come back to the code of the question.
Since you are calculating the time until midnight there is no point in storing start. Instead you should calculate the next midnight and use that as reference point. Then calculated the time between now and midnight. Both should be done for each render.
<script type="text/babel">
// time constants
const MILLISECOND = 1, MILLISECONDS = MILLISECOND;
const SECOND = 1000*MILLISECONDS, SECONDS = SECOND;
const MINUTE = 60*SECONDS, MINUTES = MINUTE;
const HOUR = 60*MINUTES, HOURS = HOUR;
const DAY = 24*HOURS, DAYS = DAY;
function nextMidnight() {
const midnight = new Date();
midnight.setHours(0, 0, 0, 0);
return midnight.valueOf() + (1*DAY);
}
function Timer({ ms }) {
const hours = Math.floor(ms / (1*HOUR));
const minutes = Math.floor(ms % (1*HOUR) / (1*MINUTE));
const seconds = Math.floor(ms % (1*MINUTE) / (1*SECOND));
const pad = (n) => n.toString().padStart(2, "0");
return <>{pad(hours)}:{pad(minutes)}:{pad(seconds)}</>;
}
class Test extends React.Component {
componentDidMount() {
this.intervalID = setInterval(() => this.forceUpdate(), 1*SECOND);
}
componentWillUnmount() {
clearInterval(this.intervalID);
}
render() {
const msUntilMidnight = nextMidnight() - Date.now();
return (
<div>
<Timer ms={msUntilMidnight} />
</div>
);
}
}
ReactDOM.render(<Test />, document.querySelector("#root"));
</script>
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/#babel/standalone#7/babel.min.js"></script>
This is not related to react, it's the browser's behavior, browser would attempt to save computing power when not focused. Probably could see this one https://stackoverflow.com/a/6032591/649322
Not sure what you mean by "lag", but if you'd like to keep as precise as possible, you could replace setInterval with setTimeout and calculate the exact time of next tick.
For example:
constructor(props) {
super(props);
this.timer = undefined;
this.prevTime = 0;
}
componentDidMount() {
const INTERVAL = 1000;
function update() {
console.log('do something');
const now = Date.now();
const delay = now - this.prevTime - INTERVAL;
const next = INTERVAL - diff;
this.setTimeout(update, next);
this.prevTime = now;
console.log('debug', Math.floor(now / 1000), delay, next);
}
update(); // fire the update
}
componentWillUnmount() {
clearTimeout(this.timer);
}
what's wrongdoing here I am creating current time to 2 minutes countdown using moment js here I am phasing issues timer stop at 1:59 not decrease time after 1:59
const CountDownTimer = () => {
const [duration, setDuration] = useState(null);
const getFinalTime = useCallback((duration) => {
const currentDuration = moment.duration(duration, "milliseconds");
let finalMinutes = currentDuration?.minutes();
let finalSeconds = currentDuration?.seconds();
finalMinutes = finalMinutes < 10 ? `0${finalMinutes}` : finalMinutes;
finalSeconds = finalSeconds < 10 ? `0${finalSeconds}` : finalSeconds;
return `${finalMinutes}:${finalSeconds}`;
}, []);
useEffect(() => {
const startTimer = moment(billData?.createdAt);
const endTimer = moment(billData?.createdAt).add(2, "minutes");
const duration = moment.duration(endTimer.diff(startTimer), "milliseconds");
const interval = setInterval(() => {
const newDuration = moment
.duration(duration - 1000, "milliseconds")
.asMilliseconds();
setDuration(newDuration);
}, 1000);
return () => clearInterval(interval);
}, [billData?.createdAt]);
// in getFinalTime passing duration
return <>{getFinalTime(duration)}</>;
};
export default CountDownTimer;
You seem to be doing a whole lot of unnecessary work for a countdown timer.
See below for a simple implementation.
The timer component accepts a deadline Date object.
The timer component has internal state that just ensures it gets updated every second by way of the effect.
Computing the time left is a matter of computing the millisecond difference between the current time and the deadline, followed by a div/mod operation to extract minutes and seconds. (You could add another div/mod to get hours and minutes.)
function CountdownTimer({ deadline }) {
const [, setUpdate] = React.useState(0);
React.useEffect(() => {
const interval = setInterval(() => {
setUpdate((u) => u + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
const timeLeftTotal = Math.max(
0,
Math.ceil((+deadline - +new Date()) / 1000)
);
const timeLeftMinutes = Math.floor(timeLeftTotal / 60);
const timeLeftSeconds = timeLeftTotal % 60;
const mmss = [timeLeftMinutes, timeLeftSeconds]
.map((s) => String(s).padStart(2, "0"))
.join(":");
return <div>Time left: {mmss}</div>;
}
function getDeadline(secondsInFuture) {
return new Date(+new Date() + secondsInFuture * 1000);
}
function App() {
const [deadline, setDeadline] = React.useState(() => getDeadline(120));
return (
<div className="App">
<CountdownTimer deadline={deadline} />
<hr />
<button onClick={() => setDeadline(getDeadline(60))}>
Reset deadline to 1 minute in the future
</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I'm trying to build a pomodoro timer with a pause option. There's an analogue clock and a digital timer. My issue is with the digital timer - I can pause it by clearing the interval but do not know how to resume it without starting from the top (with a new setInterval).
This is the codesandbox of the project.
This is the relevant part from the DigitalClock component:
const timer = () => {
const now = Date.now()
const then = now + mode.duration * 60 * 1000
countdown = setInterval(() => { // That's how I resume it (with a re-render)
const secondsLeft = Math.round((then - Date.now()) / 1000)
if(secondsLeft < 0 || pause) {
clearInterval(countdown) // That's how I pause it (by clearing the interval)
return;
}
displayTimeLeft(secondsLeft)
}, 1000)
}
I think pausing the timer could be done with a boolean instead of clearing the interval,
So let's say that u have also a boolean keeping track of if it's paused on top level
let paused = false;
and you should consider looking up for if timer is not paused then do the math inside so
countdown = setInterval(() => { // That's how I resume it (with a re-render)
if(!paused) {
const secondsLeft = Math.round((then - Date.now()) / 1000)
if(secondsLeft < 0 || pause) {
clearInterval(countdown) // That's how I pause it (by clearing the interval)
return;
}
displayTimeLeft(secondsLeft)
}
}, 1000)
The only thing that's left is to toggle this paused boolean to true/false when someone click's the Pause button.
I don't know about React that much but that would be the choice I would go if i was doing this task :)
Possible solution - don't clear the interval when it is paused, just don't update the secondsLeft on the tick
Also, secondsLeft can be an integer, it doesn't have to be related to the actual time.
// global variables
var pause = false;
var elapsed, secondsLeft = 60;
const timer = () => {
// setInterval for every second
countdown = setInterval(() => {
// if allowed time is used up, clear interval
if (secondsLeft < 0) {
clearInterval(countdown)
return;
}
// if paused, record elapsed time and return
if (pause === true) {
elapsed = secondsLeft;
return;
}
// decrement seconds left
secondsLeft--;
displayTimeLeft(secondsLeft)
}, 1000)
}
timer();
const displayTimeLeft = (seconds) => {
document.getElementById("time").textContent = seconds;
}
document.getElementById("pause").addEventListener("click", (evt) => {
pause = !pause;
evt.target.textContent = pause ? "resume" : "pause";
if (pause === false) {
secondsLeft = elapsed;
}
});
<div id="time"></div>
<button id="pause">pause</button>
Using react, the timer should be in a useEffect hook (assuming you're using functional components). The useInterval will be placed inside and will run naturally within, updating the display through the useEffect hook. Place the clear interval in the useEffect return statement so when the timer expires, the interval will clear itself.
Then using pause as a state variable, manage your timer with buttons.
const [seconds, setSeconds] = useState(30);
const [pause, setPause] = useState(false);
useEffect(() => {
const interval = setInterval(() => {
if(!pause) { //I used '!paused' because I set pause initially to false.
if (seconds > 0) {
setSeconds(seconds - 1);
}
}
}, 1000);
return () => clearInterval(interval);
});
const handlePauseToggle = () => {
setPause(!pause);
}
Add a button to click, and your pause feature is set up.
*** Side note, feel free to ignore***
It looks like you have a way to display the time already, but I think it would be easier if you use the 'seconds' state variable in curly brackets to display your timer instead of creating a function (see below).
<div>
<p>0:{seconds >= 10 ? {seconds} : `0${seconds}`}</p>
</div>
This will display the timer correctly in a single line. Minutes is easy to add and so on.
Just a thought to make your code easier.
Using custom hook
Follow the following steps:
Create a new state variable to store the counter value on resume
On start check if you have a resume value then use it otherwise use the initial counter
Here's the full custom hook:
const useCountdown = ({ initialCounter, callback }) => {
const _initialCounter = initialCounter ?? DEFAULT_TIME_IN_SECONDS,
[resume, setResume] = useState(0),
[counter, setCounter] = useState(_initialCounter),
initial = useRef(_initialCounter),
intervalRef = useRef(null),
[isPause, setIsPause] = useState(false),
isStopBtnDisabled = counter === 0,
isPauseBtnDisabled = isPause || counter === 0,
isResumeBtnDisabled = !isPause;
const stopCounter = useCallback(() => {
clearInterval(intervalRef.current);
setCounter(0);
setIsPause(false);
}, []);
const startCounter = useCallback(
(seconds = initial.current) => {
intervalRef.current = setInterval(() => {
const newCounter = seconds--;
if (newCounter >= 0) {
setCounter(newCounter);
callback && callback(newCounter);
} else {
stopCounter();
}
}, 1000);
},
[stopCounter]
);
const pauseCounter = () => {
setResume(counter);
setIsPause(true);
clearInterval(intervalRef.current);
};
const resumeCounter = () => {
startCounter(resume - 1);
setResume(0);
setIsPause(false);
};
const resetCounter = useCallback(() => {
if (intervalRef.current) {
stopCounter();
}
setCounter(initial.current);
startCounter(initial.current - 1);
}, [startCounter, stopCounter]);
useEffect(() => {
resetCounter();
}, [resetCounter]);
useEffect(() => {
return () => {
stopCounter();
};
}, [stopCounter]);
return [
counter,
resetCounter,
stopCounter,
pauseCounter,
resumeCounter,
isStopBtnDisabled,
isPauseBtnDisabled,
isResumeBtnDisabled,
];
};
And here's an example of using it on codepen:
React useCountdown