I am using a React hook to build a countdown timer and I want to use a setInterval() to decrease the time but at first it does the correct if statement (the last one) but then it keeps doing it even tough it's wrong. I tried console logging "seconds" and "minutes" and the browser keeps saying 25 and 0 (as I set them initially) even tough when I press the start button I can see the minutes showing another number.
the full code : https://pastebin.com/bHyuLGn2
The code which I say is relevant:
const [timeleft, setTimeLeft] = useState({
minutes: 25,
seconds: 0,
});
const {
minutes,
seconds
} = timeleft;
function handleStart() {
setStartButton(!startButton);
setInterval(() => {
if (seconds > 0) {
setTimeLeft((prevValue) => {
return {
...prevValue,
seconds: prevValue.seconds - 1,
};
});
} else if (seconds === 0) {
if (minutes === 0) {
clearInterval();
} else {
setTimeLeft((prevValue) => {
return {
minutes: prevValue.minutes - 1,
seconds: 59,
};
});
}
}
}, 1000);
}
No, setInterval is doing the correct thing actually, but your code is probably not doing what you want it to do.
Inside your interval, you are calling setTimeLeft. setTimeLeft causes the component to re-render, and your setInterval to reset every single time.
If you want to do what you're trying to do, you're likely going to have to pull that state out into its own hook, or architect this differently.
You using setInterval wrong, because it should be like this:
let myInterval = setInterval(() => {/* bla-bla-bla */}, 1000)
// ...
clearInterval(myInterval)
but in you code I see this:
setInterval(() => {/* bla-bla-bla */}, 1000)
// ...
clearInterval()
so, clear WHAT interval?
The problem with setInterval in react is that usually it makes the component render again, and the new render will call a new setInterval.
Here's an interesting article that provides a solution:
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
Related
I tried to create simple timer with useEffect and setInterval. In theory, on click it has to:
restart cooldown
const restartCooldown = () => {
setCooldownDuration(targetActivity['duration'])
}
useEffect() will see this and decrease cooldown every second within interval
useEffect(() => {
let interval
if (props.activity.cooldown) {
if (cooldownDuration > 0) {
if (props.activity.id === 1) {
console.log('Activity.js state', cooldownDuration)
}
interval = setInterval(() => setCooldownDuration(cooldownDuration - 1000), 1000);
} else {
clearInterval(interval)
}
} else {
setCooldownDuration(0)
}
});
useEffect() in ActivityProgression.js will get percentage of completion
useEffect(() => {
const interval = setInterval(() => setPercentages((props.totalTime - props.cooldown) / props.totalTime * 100), 1000);
})
4. Then it will be rendered
return <ProgressBar animated now={percentages} label={`${percentages}%`} />;
But in fact it doesn't work properly, creating infinity multiple renders on click.
Can you please tell me why? Much much thanks.
Full code repository to try it yourself
I tried everything, but it seems that i simply don't understand some react features :(
The second argument of useEffect is an array of dependencies. Since you didn't provide any dependencies, it runs every render. Because it updates the state, it causes a re-render and thus runs again, infinitely. Add [cooldownDuration] to your use effect like this:
useEffect(() => {...}, [cooldownDuration])
This will make the useEffect only run when cooldownDuration changes.
I want to be able to call setInterval (or something similar) at two different lengths, alternating.
For example, running a function after 5 seconds, then 1 second, then 5 seconds again, and so on.
Is this possible? I tried a function that alternates the value, but it didn't seem to work.
let num = 5000
function alternateNum() {
if (num === 5000) { num = 1000 }
else { num = 5000 }
}
setInterval(() => {
// ...
alternateNum()
}, num);
JS timers have a very complicated history.
Using a recursive setTimeout invocation is a simple and elegant solution as long (as your runtime implements tail call optimization).
Separate from the issue of recursion is the issue of timer drift. This is covered in the YouTube video JavaScript counters the hard way - HTTP 203 if you'd like an accessible introduction.
In many JS engines (e.g. V8) setInterval will handle drift correction for you, so there's actually an advantage to using it over recursively invoking setTimeout. (Check the millisecond timestamps in the console messages in the snippet below to verify this.)
In order to determine the constant interval argument you'll need for setInterval, you'll need to find the greatest common factor of your delay durations. Once you have this value, you can use it as the base interval delay, and keep track of your interval state to determine whether you should switch to the next interval delay, run your other code, etc. Here's a minimal example:
const durations = [1000, 5000];
// If you can't determine this in advance and use a constant value,
// then you can calculate it at runtime using a function:
const gcf = 1000; // or const gcf = findGreatestCommonFactor(durations);
let durationIndex = 0;
let elapsed = 0;
function update () {
elapsed += gcf;
const ready = elapsed === durations[durationIndex];
if (ready) {
elapsed = 0;
durationIndex = (durationIndex + 1) % durations.length;
}
return ready;
}
setInterval(() => {
const ready = update();
if (!ready) return;
// Do your interval task, for example:
console.log('tick');
}, gcf);
The problem with setInterval() is that the time is taken into account just once. You can use setTimeout() with recursion instead:
function doAction(flipFlop) {
setTimeout(() => {
console.log(flipFlop ? 'flip' : 'flop');
doAction(!flipFlop);
// do some other action...
}, flipFlop ? 1000 : 3000);
}
doAction(true);
Watch out though if you have a long running process, this recursion gets deeper and deeper.
I think this method is the easiest:
setInterval(() => {
console.log("first");
setTimeout(() => console.log("second"), 750);
}, 2000);
This creates an interval that alternates between 1250 and 750 milliseconds.
The problem with your code
let num = 5000
function alternateNum() {
if (num === 5000) { num = 1000 }
else { num === 5000 }
}
setInterval(() => {
// ...
alternateNum()
}, num);
The last few lines (the setInterval) call are only getting called once with the initial value of num and thus any future changes to num won't be reflected in the setTimeout call.
How to fix it
You should use setTimeout within the function that has your code and call your function recursively:
const doStuff = (time = 1000) => {
// Your code here
// generate the next time to wait
const nextTime = time === 5000 ? 1000 : 5000;
// call the function again after waiting `time` milliseconds
setInterval(() => doStuff(nextTime), time);
}
Then you would call doStuff to start it. If you wanted to start it immediately with the next one happening after 1 second you could do:
doStuff();
Or if you wanted to call it after 5 seconds with the next one happening one second after that:
setTimeout(doStuff, 5000);
The difference here compared to your code is that the variable that represents the time is being used over and over again as it changes instead of just once on initial code execution.
I created a function in which when user click start button timer will start, but it's not working. Can someone tell me why it's not working? please
that's the function I created
const [time,setTime] = useState(0)
const timeout = setInterval(() => {
if (time !== 60) {
setTime(prevState => prevState + 1);
}
}, 1000);
console.log(timeout);
return () => {
if (time == 60) {
clearTimeout(timeout);
}
};```
You could declare the Timer State as 60 instead of 0 and
const [state,updateState] = useState({timer:60})
then call this in updateState: ({timer: timer - 1})
To answer your question:
why is my code not working?
Your state timer starts out being 0 and will therefore never reach inside the if statement.
As Matt U pointed out you most likely want to use setInterval since it runs the function you pass at every X milliseconds (1000 in your case) until you stop it.
See the following for more information regarding that:
setTimeout: https://www.w3schools.com/jsref/met_win_settimeout.asp
setInterval: https://www.w3schools.com/jsref/met_win_setinterval.asp
What yesIamFaded answered should do the job in your use case, though it would be better to make use of updateState's argument prevState (or whatever you want to call it). updateState will receive the previous value and use that to compute a new value.
const [state, updateState] = useState({ timer: 60 })
const interval = setInterval(() => {
if (state.timer > 0) {
updateState(prevState => { timer: prevState.timer - 1 });
}
}, 1000);
You can read more about functional updates here:
https://reactjs.org/docs/hooks-reference.html#functional-updates
And lastly, you should clear the timeout and/or interval once you don't need it anymore using either clearTimeout() or clearInterval().
See the following for more information here:
clearTimeout: https://www.w3schools.com/jsref/met_win_cleartimeout.asp
clearInterval: https://www.w3schools.com/jsref/met_win_clearinterval.asp
P.S.
If your timer state isn't coupled with any other state I wouldn't put it into an object. Instead I would do the following:
const [timer, setTimer] = useState(60)
const interval = setInterval(() => {
if (timer > 0) {
setTimer(prevTimer => prevTimer - 1 );
}
}, 1000);
That way you won't have an unnecessary object.
I'm working on React pomodoro clock and I got stuck on joining if/else with setTimeout(). Here is my code:
React.useEffect(() => {
if(props.isOn === true) {
if(props.timeLeft <= 0) {
setTimeout(function() {
if(props.activeClockType === 'Session') {
props.handleClockType('Break');
props.handleTimeLeft(props.breakLength * 60);
} else {
props.handleClockType('Session');
props.handleTimeLeft(props.sessionLength * 60);
}
}, 1000);
}
const id = setInterval(count, 1000);
return () => clearInterval(id);
}
});
When the clock goes to 00:00 it's stopping for 1sec and then showing Break and timer sets to breakLength. Great, but after 1sec it's not counting down but changing back to Session and counting sessionLength.
When I delete setTimeout (commented in CodePen) it works good but it's not stopping at 00:00 for 1sec but immediately changes on Break what looks ugly.
How to make it stop on 00:00 for 1 second before counting other activeClockType?
I've spent whole evening on looking for any clue but still I don't have any idea what is wrong. I tried to put setTimeout in every possible combination with if but effect is always the same. From this topic I suspect that I should probably use clearTimeout but also tried it in any combination and got nothing.
Or maybe I should try another possibility to run countdown than useEffect()? Earlier I made working timer in class component (commented in CodePen) with componentWillReceiveProps but it's deprecated and my attempts to change it on other functions gave nothing. I don't fully understand this Effect Hook but it was the only way I found that is counting well. And it's shorter than class component.
Here is my full code on CodePen
Thank you in advance.
I think there is two problem here:
1) Every time you use the hook to set the state, you fired the React.useEffect, if you set the state twice, the React.useEffect would be fired twice.
2) use setTimeout to replace setInterval to avoid update state too many times.
Problems are showing below:
React.useEffect(() => {
if(props.isOn === true) {
if(props.timeLeft <= 0 && !changeClockType) {
setTimeout(function() {
if(props.activeClockType === 'Session') {
props.handleClockType('Break'); // ==> Fired update and then excuted React.useEffect
props.handleTimeLeft(props.breakLength * 60); // ==> Fired update again and then excuted React.useEffect again
} else {
props.handleClockType('Session');
props.handleTimeLeft(props.sessionLength * 60);
}
}, 1000);
}
const id = setTimeout(count, 1000); // ==> modify setInterval to setTimeout
return () => clearInterval(id);
}
});
I try to modify the code like this:
let changeClockType = false; // ==> Should be put here because it couldn't be put in the function component
function Timer(props) {
const count = () => props.handleTimeLeft(props.timeLeft - 1);
function startStop() {
props.handleIsOn(props.isOn === false ? true : false);
}
React.useEffect(() => {
console.log("step 1");
if(props.isOn === true) { console.log("step 2");
if(props.timeLeft <= 0 && !changeClockType) { console.log("step 3");
setTimeout(function() {
if(props.activeClockType === 'Session') { console.log("step 4");
changeClockType = true;
props.handleClockType('Break'); // ==> Fired update and then excuted React.useEffect
console.log("Change to Break")
props.handleTimeLeft(props.breakLength * 60); // ==> Fired update again and then excuted React.useEffect again
changeClockType = false;
console.log("Set breakLength")
} else { console.log("step 5");
changeClockType = true;
props.handleClockType('Session');
props.handleTimeLeft(props.sessionLength * 60);
changeClockType = false;
}
}, 1000);
}
const id = setTimeout(count, 1000); // ==> modify setInterval to setTimeout
return () => clearInterval(id);
}
});
The code isn't so clean but can be run well.
I'm trying to create a timer with the code below, but when I console log time in the tick function all I get back is 60 continuously , if I pass the console.log after time - 1 I get NaN , which I guess means that time for some reason is not processed as an integer.
I cannot use state as using state for this timer re renders my components again and again which makes everything go crazy.
componentWillMount() {
var time = 60;
this.interval = setInterval(this.tick.bind(this, time), 1000);
}
tick(time) {
time = Number.parseInt(time) // This does nothing for some reason
console.log(time);
time = time - 1;
if (time <= 0) {
console.log('Hi');
} return time;
}
EDIT: I found your actual problem.
You need to store time in state. Add it to your initial state as 60, change the interval line to:
setInterval(this.tick, 1000)
and your tick function to:
tick = () => {
this.setState((prevState) => {
return {
time: prevState.time - 1
}
}, () => {
if(this.state.time <= 0){
console.log('hi')
}
})
}
This way, time is tracked outside of your function scope and the value should always be correct. By passing an anonymous callback as the second argument to setState you ensure that the check only happens after time is decremented. The one issue I'd see with this is that React can be a bit funky with running setState and may not run every second, so this could end up being slightly imprecise.
componentWillMount() {
var time = 60;
this.interval = setInterval(
(function() {
time--;
this.tick(time);
}).bind(this),
1000);
}
tick(time) {
time = Number.parseInt(time) // This does nothing for some reason
console.log(time);
//time = time - 1; //I removed this line
if (time <= 0) {
console.log('Hi');
}
return time;
}
componentWillMount would look a lot prettier if you used arrow functions
componentWillMount() {
let time = 60;
this.interval = setInterval(() => {
time--;
this.tick(time);
},
1000);
}
Now you are changing time in the right place and sending it on to the function on each call.