Reputation: 309
I am trying to render a count down timer on screen with react hooks, but I am not sure what is the best way to render it.
I know I am supposed to use the useEffect
to compare current state to previous state, but I do not think I am doing it correctly.
I would appreciate the help!
I have tried a couple of different ways, none of them work, like setting a state 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;```
Upvotes: 30
Views: 71611
Reputation: 401
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);
}
Upvotes: 2
Reputation: 556
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
Upvotes: 1
Reputation: 4561
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>;
}
Upvotes: 16
Reputation: 403
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
Upvotes: 5
Reputation: 11770
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>
);
};
Upvotes: 83