I tried to create a react timer that counts 20 seconds and then stops at 0 seconds. But the problem is it gets struck randomly in between 14,13 or 12 seconds and they keep repeating the same value again and again. Here is my code.
import React, { useEffect,useState } from 'react';
const Airdrop = () => {
const [timer,setTimer] = useState(0);
const [time,setTime] = useState({});
const [seconds,setSeconds] = useState(20);
const startTimer = () =>{
if(timer === 0 && seconds > 0){
setInterval(countDown,1000);
}
}
const countDown = ()=>{
let secondsValue = seconds - 1;
let timeValue = secondsToTime(secondsValue);
setTime(timeValue);
setSeconds(secondsValue);
if(secondsValue === 0){
clearInterval(timer);
}
}
const secondsToTime = (secs)=>{
let hours,minutes,seconds;
hours = Math.floor(secs/(60*60));
let devisor_for_minutes = secs % (60*60);
minutes = Math.floor(devisor_for_minutes/60);
let devisor_for_seconds = devisor_for_minutes % 60;
seconds = Math.ceil(devisor_for_seconds);
let obj = {
"h": hours,
"m": minutes,
"s": seconds
}
return obj;
}
useEffect(() => {
let timeLeftVar = secondsToTime(seconds);
setTime(timeLeftVar);
}, []);
useEffect(() => {
startTimer();
});
return (
<div style={{color:"black"}}>
{time.m}:{time.s}
</div>
)
}
export default Airdrop
CodePudding user response:
You are creating a new interval each time the component is rendered because one of the useEffect hooks is missing a dependency array.
const startTimer = () => {
if (timer === 0 && seconds > 0) {
setInterval(countDown,1000);
}
}
useEffect(() => {
startTimer();
});
I suggest the following:
- Capture the timer id in a React ref
- Remove the
timestate, it is derived state from thesecondsstate, just compute it each render cycle - Add a dependency to the
useEffecthook starting the timer
Code
const Airdrop = () => {
const timerRef = useRef(null);
const [seconds, setSeconds] = useState(20);
const startTimer = () => {
if (timerRef.current === null && seconds > 0) {
timerRef.current = setInterval(countDown, 1000); // <-- capture timer id to stop interval
}
};
const countDown = () => {
setSeconds((seconds) => seconds - 1); // <-- interval callback only decrement time
};
const secondsToTime = (secs) => {
let hours, minutes, seconds;
hours = Math.floor(secs / (60 * 60));
let devisor_for_minutes = secs % (60 * 60);
minutes = Math.floor(devisor_for_minutes / 60);
let devisor_for_seconds = devisor_for_minutes % 60;
seconds = Math.ceil(devisor_for_seconds);
let obj = {
h: hours,
m: minutes,
s: seconds
};
return obj;
};
useEffect(() => {
if (seconds === 0) {
clearInterval(timerRef.current);
timerRef.current = null;
setSeconds(20);
}
}, [seconds]); // <-- check seconds remaining to kill interval/reset
useEffect(() => {
startTimer();
}, []); // <-- only start timer once
const time = secondsToTime(seconds); // <-- compute time
return (
<div style={{ color: "black" }}>
{time.m}:{time.s}
</div>
);
};
CodePudding user response:
When you are using multiple state and they each depend on each other it is more appropriate to use a reducer because state updates are asynchronous. You might update one state based on a stale value.
However in your case, we don't need a reducer because we can derive all the data we need from a single state.
When you set the state, it is not immediately updated and it might cause issues when the next state depend on the last one. Especially when you use it like this:
const newState = state-1;
setState(newState);
However with functional updates we can directly use the last state to set the next one.
setState((prevState)=> prevState-1);
I took the liberty of creating helper functions to make your component learner. You copy them in a file in /helpers and import them directly.
import React, { useEffect, useState } from 'react';
const getHours = (duration) => {
const hours = Math.floor(duration / 3600);
if (hours < 10) return '0' hours.toString();
return hours.toString();
};
const getMinutes = (duration) => {
const minutes = Math.floor((duration - getHours(duration) * 3600) / 60);
if (minutes < 10) return '0' minutes.toString();
return minutes.toString();
};
const getSeconds = (duration) => {
const seconds =
duration - getHours(duration) * 3600 - getMinutes(duration) * 60;
if (seconds < 10) return '0' seconds.toString();
return seconds.toString();
};
const Airdrop = (props) => {
const { duration = 20 } = props;
const [seconds, setSeconds] = useState(duration);
useEffect(() => {
if (seconds === 0) return;
const timeOut = setTimeout(() => {
setSeconds((prevSeconds) => {
if (prevSeconds === 0) return 0;
return prevSeconds - 1;
});
}, 1000);
return () => {
clearTimeout(timeOut);
};
}, [seconds]);
return (
<div style={{ color: 'black' }}>
{getHours(seconds)}:{getMinutes(seconds)}:{getSeconds(seconds)}
</div>
);
};
export default Airdrop;
