creating timer with react - reactjs

I want to have a countdown timer that counts from 60 to zero when the stage of program reaches two. Here's my code:
const [timer, setTimer] = useState(60)
useEffect(() => {
if (stage === 2) {
const interval = setInterval(() => {
console.log(timer)
setTimer(timer - 1)
if (timer === 1) {
clearInterval(interval)
}
}, 1000)
}
}, [stage])
and i have a div like below that just shows the counter value
<div>{timer}</div>
in my setInterval, when i use console.log(timer) it always prints out 60. But inside the div, the value starts with 60 and in the next second it will always be 59.
What am i doing wrong here?

You have closure on time === 60 value, use functional update instead.
For the same reason, you should have another useEffect for canceling the interval:
const intervalId = useRef();
useEffect(() => {
if (stage === 2) {
intervalId.current = setInterval(() => {
setTimer((prevTimer) => prevTimer - 1);
}, 1000);
}
}, [stage]);
useEffect(() => {
console.log(timer);
if (timer === 1) {
clearInterval(intervalId.current);
}
}, [timer]);
Check similar question: setInterval counter common mistakes.

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]);

Increase the counter, then reset it when a certain limit is reached

I just started learning react and I have about a performance question. I want to increment a counter using the setInterval function and then reset it when the length of the array is reached. The problem is I need to add an active variable for the useEffect dependency, which removes the interval and then creates it again.
useEffect(() => {
const timer = setInterval(() => {
active == arr.length - 1 ? setActive(0) : setActive((active) => active + 1)
}, 3000)
return () => clearInterval(timer)
}, [active])
So, I wrote code like this, which looks crazy, but does the same job without removing the interval, and gets the actual version of the active variable from the callback.
useEffect(() => {
const timer = setInterval(() => {
setActive((active) => (active === arr.length - 1 ? active == 0 : active + 1))
}, 3000)
return () => clearInterval(timer)
}, [])
The question is how best to write and not do unnecessary work in the component
I would probably split all this logic out into a couple effects.
One to manage incrementing active on the interval and clearing the interval when the component unmounts.
useEffect(() => {
const timer = setInterval(() => {
setActive(active => active + 1);
}, 3000);
return () => clearInterval(timer);
}, []);
A second to reset the state on the condition.
useEffect(() => {
if (active === arr.length - 1) {
setActive(0);
}
}, [active, arr]);
I would also caution you to protect against the edge case where active is updated to 0 and the arr array is an array of length 1 as this will trigger render looping.

React useEffect: how to clearInterval?

I made a count down button.
How could I clearInterval when the button will be unmounted? (like componentWillUnmount)
const CooldownButton = ({
cooldown,
...props
}) => {
const defaultClasses = useStyles();
const [count, setCount] = useState(cooldown);
const [timer, setTimer] = useState(null);
useEffect(() => {
if (count > 0 && !timer) {
setTimer(
setInterval(() => {
if (count > 0) {
setCount((prevState) => prevState - 1);
}
if (count === 0) {
clearInterval(timer);
setTimer(null);
}
}, 1000)
);
}
}, [count, timer]);
useUpdateEffect(() => {
setCount(cooldown);
}, [cooldown]);
return (
// ...
<Typography
size={24}
className={clsx(defaultClasses.counter, classes?.counter)}
{...counterProps}
>
{`${new Date(count * 1000)
.toISOString()
.substr(...(count >= 3600 ? [11, 8] : [14, 5]))}`}
</Typography>
// ...
);
};
This will cause an infinite render:
useEffect(() => {
if (count > 0 && !timer) {
// ...
}
return () => {
clearInterval(timer);
setTimer(null);
}
}, [count, timer]);
if just wanner auto countdown, do not need setInterval,
when count changed, useEffect will run
const [count, setCount] = useState(cooldown);
useEffect(() => {
if (count > 0) {
const timer = setTimeout(()=> {
setCount(count - 1);
}, 1000);
return ()=> clearTimeout(timer);
}
}, [count, timer]);
You are adding setTimer in return and at the same time you are adding timer variable in dependency array which will cause infinite rendering. Because this useEffect will trigger whenever there is change in either count or timer.
In your case you are changing timer in useEffect itself which is causing infinite rendering. So, remove timer from dependency array (or remove setTimer function in case you don't need it) and try!
useEffect(() => {
if (count > 0 && !timer) {
// ...
}
return () => {
clearInterval(timer);
setTimer(null);
}
}, [count]);

React. Fill a state array every N seconds, and only while its smaller than X length

I have the following code
import * as React from "react";
import { useState, useEffect } from "react";
const TxContainer: React.FunctionComponent = (props) => {
const [tx, setTx] = useState<Array<string>>([]);
useEffect(() => {
const interval = setInterval(() => {
setTx((oldArr) => [...oldArr, "tx" + Math.random()]);
}, Math.floor(Math.random() * 3000) + 1000);
return () => clearInterval(interval);
}, [tx.length < 10]); //this useEffect still keeps pushing even if the array is bigger than 9
useEffect(() => {
console.log(tx.length);
}, [tx.length]);
let listTx = tx.map((data, index) => (
<p key={index}>
{index} {data}
</p>
));
return <React.Fragment>{listTx}</React.Fragment>;
};
export default TxContainer;
Im trying to create random strings and put them in an array until this array is 10. Whenever, its 10 it should start deleting the first element of the array to keep working and displaying new data, but thats another story.
The point is that its not stopping when it should.
When you are passing a compare like that in the dependency array of the useEffect it will only invoke it again, when the resulting value changes. Basically those dependencies mean "if this value changes in any way, call the useEffect". Besides that it's being called on mount and on unmount. So for your code to work as you intended you should pass to the dependency array a variable that your calculations depend on and pass the logic inside the useEffet.
Here's an example of the code that you described, of course I don't know if you want to use the same timeout or not.
useEffect(() => {
let interval;
if (tx.length < 5) {
interval = setInterval(() => {
setTx(oldArr => [...oldArr, "tx" + Math.random()]);
}, Math.floor(Math.random() * 3000) + 1000);
} else {
interval = setInterval(() => {
setTx(oldArr => [...oldArr.slice(1)]);
}, Math.floor(Math.random() * 3000) + 1000);
}
return () => clearInterval(interval);
}, [tx.length ]);`
Here is the solution: second useEffect is not needed. This effect runs first time and whenever array length is changed. when it reaches 11, clear the interval or remove first element.
useEffect(() => {
const interval = setInterval(() => {
setTx((oldArr) => [...oldArr, "tx" + Math.random()]);
}, Math.floor(Math.random() * 3000) + 1000);
if(tx.length === 11){
setTx(prev=>prev.slice(1))
// clearInterval(interval)
}
return () => clearInterval(interval);
}, [tx.length]);

Resources