counterdown timer in react dont work properly - reactjs

I am beginner.
I have a question, I have made a counter timer in React, but unfortunately, it doesn't work properly.
I can't find a mistake, Could someone help me?
import React, { useState, useEffect } from "react";
export default function CountDown() {
let [seconds, setSeconds] = useState(3);
let [minutes, setMinutes] = useState(59);
let [hours, setHours] = useState(3);
useEffect(() => {
const interval = setInterval(() => {
setCounddownTimer();
console.log("i am working", { seconds, minutes, hours });
return () => clearInterval(interval);
}, 1000);
}, []);
const setCounddownTimer = () => {
if (hours === 0 && minutes === 0 && seconds === 0) {
timerReset();
} else if (minutes === 0 && seconds === 0) {
console.log({ seconds, minutes, hours });
setHours(--hours);
setMinutes(59);
setSeconds(59);
} else if (seconds === 0) {
setSeconds(59);
setMinutes(--minutes);
} else {
setSeconds(--seconds);
}
};
const timerReset = () => {
setSeconds(59);
setMinutes(59);
setHours(3);
};
const addLeadingZero = (number) => {
return number < 10 ? "0" + number : number;
};
const style = {
"text-align": "center",
"font-weight": "bold",
color: "#cf0000"
};
return (
<div style={style}>
{addLeadingZero(hours)}:{addLeadingZero(minutes)}:
{addLeadingZero(seconds)}
</div>
);
}
I am also paste a link to codesandbox https://codesandbox.io/s/epic-hopper-f9dy4?file=/src/App.js:0-1277

Your useEffect's return statement was inside the setInterval(). It should be outside.
const interval = setInterval(() => {
setCounddownTimer();
console.log("i am working", { seconds, minutes, hours });
}, 1000);
return () => clearInterval(interval);
UPDATE
Also, you should've put hours, minutes, seconds in useEffect's dependency array, since rendering depends on these 3 states. Otherwise the seconds will count down till the timer reaches 03:58:59, but then the minutes will start counting down.

Related

How to clearInterval correctly using React Native

In RN, I have a countdown timer using setInterval that goes from 10 - 0.
Once the condition is met of the time === 0 or less than 1, I want the interval to stop.
The countdown is working but is repeating continuously, clearInterval not working.
What am I doing wrong?
import { StyleSheet, Text } from 'react-native'
import React, {useEffect, useState} from 'react'
export default function Timer() {
const [time, setTime] = useState(10)
useEffect(() => {
if(time > 0) {
var intervalID = setInterval(() => {
setTime(time => time > 0 ? time - 1 : time = 10)
}, 1000)
} else {
clearInterval(intervalID)
}
}, [])
return <Text style={styles.timer}>{time}</Text>
}
ClearInterval should be inside, setTime because useEffect will only trigger once.
NOTE: you should also clear on unmount.
export default function Timer() {
const [time, setTime] = useState(10);
useEffect(() => {
var intervalID = setInterval(() => {
setTime((time) => {
if (time > 0) {
return time - 1;
}
clearInterval(intervalID);
return time;
});
}, 1000);
return () => clearInterval(intervalID);
}, []);
return <Text style={styles.timer}>{time}</Text>;
}
The function is on useEffect that only get executed when the component is mounted/unmounted, at the moment where the function is executed, time is 10 so will never go into else condition.
This code must work for you:
import { StyleSheet, Text } from 'react-native'
import React, {useEffect, useState} from 'react'
export default function Timer() {
const [time, setTime] = useState(10)
time <= 0 && clearInterval(intervalID)
useEffect(() => {
var intervalID = setInterval(() => {
setTime(time => time > 0 ? time - 1 : time = 10)
}, 1000)
return () => {
clearInterval(intervalID)
}
}, [])
return <Text style={styles.timer}>{time}</Text>
You can use this also:
import React, { useEffect, useState, useRef } from 'react'
import { Text } from 'react-native'
const Timer = () => {
const [time, setTime] = useState(10)
const promiseRef = useRef(null)
useEffect(() => {
if(time > 0) {
promiseRef.current = setInterval(() => {
setTime(time => time > 0 ? time - 1 : time = 10)
}, 1000)
} else {
promiseRef.current = null
}
}, [])
return <Text style={styles.timer}>{time}</Text>
}
export default Timer
Your useEffect does not have any value(time) inside the dependency array, so it will run only once on component mount.
const [time, setTime] = useState(10);
useEffect(() => {
if (time === 0) {
return;
}
const timeoutId = setInterval(() => {
setTime(time - 1);
}, 1000);
return () => {
clearInterval(timeoutId);
};
}, [time]);
You could also use timeout, where you don't need to clear anything as it will run only once per useEffect trigger.
useEffect(() => {
if (time === 0) {
return;
}
setTimeout(() => {
setTime(time - 1);
}, 1000);
}, [time]);
Another option that might work if you don't want to add time to the dependency array is to clear the interval inside the setTime
useEffect(() => {
const intervalID = setInterval(() => {
setTime((time) => {
if (time === 0) {
clearInterval(intervalID);
return time;
}
return time - 1;
});
}, 1000);
return () => {
clearInterval(intervalID);
};
}, []);
Note that in your example you are counting down to 1 and the going back to 10, so it will never reach 0
setTime(time => time > 0 ? time - 1 : time = 10)
if you want it to reset back to 10 after it finishes count the last option might work for you, as it won't trigger the effect on every time change, just need to return 10 instead of time after clearing the interval
Here is an example https://jsfiddle.net/dnyrm68t/

Not clear why the use effect hook gets triggered too many times

I am trying to build a timer with react
for some sort of reason, it is not incrementing properly - the use effect hook gets triggered too many times and I do know why is that happening
instead of incrementing in 1 second intervals it in increments 3 or more second intervals
Maybe you explain why the hook gets triggered so many times and what i could do to resolve the issue
const Timer = () => {
const [timeDisplayed, setTimeDisplayed] = useState(0);
const [startTime, setStartTime] = useState(0)
const [timerOn, setTimerOn] = useState(false);
React.useEffect(() => {
let interval = null;
if (timerOn) {
interval = setInterval(() => {
var delta = Date.now() - startTime;
setTimeDisplayed(timeDisplayed + Math.floor(delta / 1000))
}, 1000);
} else if (!timerOn) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [timerOn, timeDisplayed]);
const start = () => {
setStartTime(Date.now())
setTimerOn(true)
}
const stop = () => {
setTimerOn(false)
}
const reset = () => {
setTimerOn(false)
setTimeDisplayed(0)
}
return (
<div >
<h2>Timer</h2>
<p>{timeDisplayed}</p>
<div id="buttons">
<button disabled={timerOn} onClick={() => {start()}}>{timeDisplayed===0 ? 'Start' : 'Resume'}</button>
<button disabled={!timerOn} onClick={() => stop()}>Stop</button>
<button disabled={timeDisplayed === 0} onClick={() => reset()}>Reset</button>
</div>
</div>
);
};
export default Timer;```
When you set timerOn you start updating timeDisplayed every second, and because your effect specifies timeDisplayed as a dependency it runs again. timerOn is still true, so it calls setInterval again, and now you have it updating twice every second. Then three times. Four. And so on.
You could fix this by either 1) returning a cleanup function that clears the interval, or 2) setting interval back to null when the timer is off and adding it to your start condition:
if (timerOn && !interval) {
interval = setInterval(() => {
var delta = Date.now() - startTime;
setTimeDisplayed(prev => prev + Math.floor(delta / 1000))
}, 1000);
} else if (interval) {
clearInterval(interval);
interval = null;
}
}, [timerOn, timeDisplayed]);
You useEffect is updating state that it is dependent on. Meaning it calls itself recursively.
Make use of the callback function of your state setter setTimeDisplayed, and remove timeDisplayed from your dependency array.
React.useEffect(() => {
let interval = null;
if (timerOn) {
interval = setInterval(() => {
var delta = Date.now() - startTime;
setTimeDisplayed(prev => prev + Math.floor(delta / 1000))
}, 1000);
} else if (!timerOn) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [timerOn]);
Here issue is not with the useEffect hook. It's in the logic you are using to update value of timeDisplayed. You are adding previous value with new updated count.
For example :
when timeDisplayed = 1, Math.floor(delta / 1000) returns 2. That's why on next update timeDisplayed'svalue is set to 3 (timeDisplayed + Math.floor(delta / 1000)) instead of 2.
Try adding a console.log statement as shown below to see all values at each update.
React.useEffect(() => {
let interval = null;
if (timerOn) {
interval = setInterval(() => {
var delta = Date.now() - startTime;
console.log('vals',timeDisplayed, Math.floor(delta / 1000),timeDisplayed + Math.floor(delta / 1000) )
setTimeDisplayed(timeDisplayed + Math.floor(delta / 1000))
}, 1000);
} else if (!timerOn) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [timerOn, timeDisplayed]);
Update you useEffect to this and your problem should be solved.
React.useEffect(() => {
let interval = null;
if (timerOn) {
interval = setInterval(() => {
var delta = Date.now() - startTime;
setTimeDisplayed(Math.floor(delta / 1000))
}, 1000);
} else if (!timerOn) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [timerOn, timeDisplayed]);

React Timer value get struck after it reaches 12 sec

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
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 leaner. You can copy them in a file in /helpers and import them directly.
I also replaced the interval with a timeout because it is easy get a situation where we don't clear the interval. For example the component is unmounted and without finishing the timer. We then get an interval running indefinitely.
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;
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, use a mounting useEffect hook to return a cleanup function to clear any running intervals if/when component unmounts
Remove the time state, it is derived state from the seconds state, just compute it each render cycle
Add a dependency to the useEffect hook starting the timer
Move secondsToTime utility function outside component
Code
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);
return {
h: hours,
m: minutes,
s: seconds
};
};
...
const Airdrop = () => {
const timerRef = useRef(null);
const [seconds, setSeconds] = useState(20);
useEffect(() => {
return () => clearInterval(timerRef.current); // <-- clean up any running interval on unmount
}, []);
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
};
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>
);
};

Is there a reason why my clearInterval function isnt working in React?

I've created a simple timer script initialized with 10 seconds for testing. The problem I'm having is that the pause timer button isn't working as expected.
import React, { useState } from 'react';
function App() {
const [time, updateTime] = useState(() => { return 10 });
const [timerRunning, setTimerRunning] = useState(false);
let minutes = Math.floor(time / 60);
let seconds = time % 60;
let interval;
function startTimer() {
setTimerRunning(true);
interval = setInterval( function() {
updateTime(previousTime => previousTime === 0 ? previousTime : previousTime - 1);
}, 1000);
}
function pauseTimer() {
setTimerRunning(false);
clearInterval(interval);
}
function restartTimer() {
setTimerRunning(false);
updateTime(() => {return 10});
}
return (
<>
<p>{minutes > 9 ? minutes : "0" + minutes}:{seconds > 9 ? seconds : "0" + seconds}</p>
<button onClick={startTimer}>Start</button>
<button onClick={pauseTimer}>Pause</button>
<button onClick={restartTimer}>Restart</button>
</>
)
}
export default App;
I want the pause button to pause the timer. Eventually I'll make conditional statements to have each button appear based on the state of the app and the value of time, but the pause button is my current obstacle.
I first had a separate countdown function which used a conditional to stop the time when the time matched counter (below). I thought of a less complicated way that lets me omit the counter variable (above). Im not sure which option is better, or if either is preventing the clearInterval function to work properly. The clearInterval function works within the countdown function if statement, but will not work outside of it.
import React, { useState } from 'react';
function App() {
const [time, updateTime] = useState(() => { return 10 });
const [timerRunning, setTimerRunning] = useState(false);
let counter = 0;
let minutes = Math.floor(time / 60);
let seconds = time % 60;
let interval;
function countdown() {
counter++;
if ( counter === time ) {
setTimerRunning(false);
clearInterval(interval);
}
updateTime(previousTime => previousTime - 1);
}
function startTimer() {
setTimerRunning(true);
interval = setInterval(countdown, 1000);
}
function pauseTimer() {
setTimerRunning(false);
clearInterval(interval);
}
function restartTimer() {
setTimerRunning(false);
updateTime(() => {return 10});
}
return (
<>
<p>{minutes > 9 ? minutes : "0" + minutes}:{seconds > 9 ? seconds : "0" + seconds}</p>
<button onClick={startTimer}>Start</button>
<button onClick={pauseTimer}>Pause</button>
<button onClick={restartTimer}>Restart</button>
</>
)
}
export default App;
Basically you can't create let interval; and assign it a setInterval like interval = setInterval(countdown, 1000);
because on each re-render there will be new let interval;
what you need to do is create a variable which isn't change on re-redners, you can use useRef
const interval = useRef(null);
.....
function startTimer() {
interval.current = setInterval(countdown, 1000);
}
....
function pauseTimer() {
clearInterval(interval.current);
}
and I don't think you need const [timerRunning, setTimerRunning] = useState(false);
find a demo here
Basically when functional component re-renders it will execute from top to bottom, if you use like let counter = 0;, then on each re-render it will initialize to 0, if you need to persists your values in each re-renders you might need some hooks (Ex: useState, useRef ...), in this case useRef would do the trick (because you need only one setInterval in each re-renders and useRef will give you the previous value, it will not re-initalize like a general variable)
You have to use useEffect, like this:
const handleStart = () => {
setChangeValue(true)
}
const handlePause = () => {
setChangeValue(false)
pauseTimer()
}
const handleRestart = () => {
setInitialState()
setChangeValue(true)
}
useEffect(() => {
if (changeValue) {
const interval = setInterval(() => {
startTimer()
}, 100)
return () => clearInterval(interval)
}
}, [changeValue])
you have three buttons to start, pause and restart, invoke these (handleStart, handlePause, handleRestart) functions with them
that is my solution
instead of the startTime function, I use useEffect
useEffect(()=>{
interval = timerRunning && setInterval(() => {
updateTime(previousTime => previousTime === 0 ? previousTime : previousTime - 1);
}, 1000);
return ()=> clearInterval(interval)
},[timerRunning])
and in onClick Start Button
<button onClick={()=> setTimerRunning(true)}>Start</button>
I hope it is useful

Having Issues Creating a Countdown Timer, which should start a new timer whenever the last timer ends(Snack Link Included!)

So I'm trying to create a timer that allows a user to: start, pause, and resume.
Also, I want the timer to start and run for 10 minutes, and once that's done, reset and run for 5 minutes, and then reset and run for another 10minutes and then stop.
The issue with my code is that the clearInterval(interval) isn't stopping the timer as expected when the conditions I'm looking for are met. The block of code is being accessed, and the console log within in runs, but my timer will keep going and will even let the minutes go negative :(
Here's the snack and code:
import React, { useState, useEffect } from 'react';
import { View, Text } from 'native-base';
const Timer = ({ timerStarted, toggleTimer }) => {
const [minutes, handleMinutes] = useState('10');
const [seconds, handleSeconds] = useState('00');
const [secondsLeft, handleSecondsLeft] = useState();
const [timerType, handleTimerType] = useState('1st'); // 2nd, 3rd
console.log('timer type', timerType);
useEffect(() => {
let interval = null;
const timerEnded = minutes === '00' && seconds === '00';
console.log('useEffect Upper Level', minutes, seconds);
if (timerStarted && !timerEnded) {
console.log('timer starting', minutes, seconds);
interval = setInterval(() => {
decrementClock();
}, 100);
}
if (timerEnded && timerType === '3rd') {
clearInterval(interval);
// toggleTimer()
console.log('DONE');
}
if (timerEnded && timerType === '2nd') {
clearInterval(interval);
handleMinutes('10');
// toggleTimer();
console.log('interval?', interval);
handleTimerType('3rd');
console.log('SECOND CONDITION');
}
if (timerEnded && timerType === '1st') {
console.log('FIRST CONDITION');
console.log('interval?', interval);
clearInterval(interval);
handleMinutes('05');
// toggleTimer();
handleTimerType('2nd');
}
if (!timerStarted) {
console.log('paused?');
clearInterval(interval);
}
return () => clearInterval(interval);
}, [timerStarted, seconds, minutes, secondsLeft, timerType]);
useEffect(() => {
let result = minutes * 60 + parseInt(seconds);
timerStarted && handleSecondsLeft(result);
}, [timerStarted]);
const decrementClock = () => {
let min = Math.floor(secondsLeft / 60);
let sec = secondsLeft - min * 60;
if (min < 10) {
min = '0' + min;
}
if (sec < 10) {
sec = '0' + sec;
}
handleMinutes(min);
handleSeconds(sec);
handleSecondsLeft(secondsLeft => secondsLeft - 1);
};
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text style={{ textAlign: 'center', fontSize: 40 }}>
{minutes}:{seconds}
</Text>
</View>
);
};
const TimerScreen = () => {
const [timerStarted, handleTimerStarted] = useState(false);
const toggleTimer = () => {
handleTimerStarted(!timerStarted);
};
<View style={{ flex: 1 }}>
<Timer timerStarted={timerStarted} toggleTimer={toggleTimer} />
</View>;
};
useEffect(() => {
}, [timerStarted, seconds, minutes, secondsLeft, timerType]);
Above code will be triggered every time when timerStarted, seconds, minutes, secondsLeft, timerType updated. I think it is not good because it will affect to your interval. You should separate your timer out.
You could use useInterval hooks in this tutorial https://overreacted.io/making-setinterval-declarative-with-react-hooks/
useInterval(() => {
decrementClock();
}, timerStarted ? 1000 : null); // If timerStarted, run interval. Otherwise, reset it

Resources