I'm attempting to make a basic count-down timer in React. It should start at 30 seconds, and count down by 1 each second. When clicking the button, the timer should restart at 30 and begin the countdown again.
This seems simple enough, and it's printing to the console exactly as I expect it to. However, as soon as I try to update state with my timer so I can render the countdown on-screen (uncomment the commented line below) the console.log duplicates, the render doubles, and I seem to have two states running simultaneously. I'm not sure what to do about this.
Any help is greatly appreciated!
const [seconds, setSeconds] = useState(30)
let interval = null
function startTimer() {
stopTimer()
let start = 30
interval = setInterval(() => {
// setSeconds(start)
console.log('start: ', start)
start--
}, 1000)
}
function stopTimer() {
clearInterval(interval)
}
return (
<p>{seconds}s</p>
<button onClick={startTimer}>Start</button>
)
I've looked around to see what I could find myself before posting. I read through a number of articles on React and setInterval, and watched some tutorials, but couldn't find what I was looking for. I attempted to rewrite the code in different ways but always ended with the same result.
There are multiple things to say, like why use async/await when there is nothing to await for, why use a local variable start = 30 when you just want to decrease your seconds count and why you declare the interval in the function body. A React functional component will run all its code and in your case do let interval = null everytime it rerenders. You have to store the interval somewhere else, like here as a global variable. Moreover, when you create the setInterval, it won't have access to the new seconds count. What you can do is use the arrow function form inside your setState function. Doing so, you will get the right seconds variable.
Maybe the code below will help you find out what's wrong:
let interval = null
function App(props) {
const [seconds, setSeconds] = React.useState(30)
function startTimer() {
stopTimer()
interval = setInterval(() => {
setSeconds((seconds) => seconds - 1)
}, 1000)
}
function stopTimer() {
clearInterval(interval)
setSeconds(30)
}
return (<button onClick={startTimer}>{seconds}</button>)
}
Thanks for the help folks!
Turns out, regardless of how I assign the state to seconds (arrow function or a separate variable) the cause of the issue here was the placement of the 'interval' variable.
Thomas's answer was correct. Moving it outside of the functional component rather than inside corrected the issue. If the variable was within the function the interval seemed like it didn't fully clear, just paused, and then there were two intervals running simultaneously.
Here's the final code, functioning as expected.
import { useState } from "react"
let interval = null
export default function app() {
const [seconds, setSeconds] = useState(30)
function startTimer() {
stopTimer()
interval = setInterval(() => {
setSeconds((seconds) => seconds - 1)
}, 1000)
}
function stopTimer() {
clearInterval(interval)
setSeconds(30)
}
return (
<p>{seconds}s</p>
<button onClick={startTimer}>Start</button>
</div>
)
}
Related
I'm creating a simple countdown timer app with React and I am having hard time understanding how setInterval works while React re-renders the component.
For example, this following code had timer continue to run even though I had used clearInterval onPause().
let startTimer;
const onStart = () => {
startTimer = setInterval( ()=>{
if ( timeRemaining === 0 ) {
clearInterval(startTimer);
setIsCounting(false)
return
}
updateTimer()
}, 1000)
setIsCounting( (prev) => !prev )
} // end of onStart
const onPause = () => {
setIsCounting( (prev) => !prev )
clearInterval(startTimer)
}
return (
{ props.isCounting ?
<button onClick={props.onPause}> Pause </button>
: <button onClick={props.onStart}> Start </button> }
)
However, the timer successfully pauses when I simply change
let starter;
to
let startTimer = useRef(null)
const onStart = () => {
startTimer.current = setInterval( ()=>{
if ( timeRemaining === 0 ) {
clearInterval(startTimer);
setIsCounting(false)
return
}
updateTimer()
}, 1000)
setIsCounting( (prev) => !prev )
} // end of onStart
const onPause = () => {
setIsCounting( (prev) => !prev )
clearInterval(startTimer.current)
}
What's happening to setInterval when React re-renders its component? Why did my timer continue to run when I didn't use useRef()?
A ref provides what's essentially an instance variable over the lifetime of a component. Without that, all that you have inside an asynchronous React function is references to variables as they were at a certain render. They're not persistent over different renders, unless explicitly done through the call of a state setter or through the assignment to a ref, or something like that.
Doing
let startTimer;
const onStart = () => {
startTimer = setInterval( ()=>{
could only even possibly work if the code that eventually calls clearInterval is created at the same render that this setInterval is created.
If you create a variable local to a given render:
let startTimer;
and then call a state setter, causing a re-render:
setIsCounting( (prev) => !prev )
Then, due to the re-render, the whole component's function will run again, resulting in the let startTimer; line running again - so it'll have a value of undefined then (and not the value to which it was reassigned on the previous render).
So, you need a ref or state to make sure a value persists through multiple renders. No matter the problem, reassigning a variable declared at the top level of a component is almost never the right choice in React.
I am trying to run a function continuously until a condition is met, but in this test case, until the off button is pressed. My first issue is that the function does not stop when i press the off button.
let intervalId
function on(){
intervalId = window.setInterval(function(){
setnum(num=>num+1)
//setnum(num + 1)
//Line 11 results in the number going up once and if i keep pressing the button it goes up by one but flashes between numbers more and more frantically every press. The off button has no effect.
//updateUserMoney()
}, 400);
}
function off(){
clearInterval(intervalId)
}
return (
<>
{num}
<button onClick={()=>on()}>On</button>
<button onClick={()=>off()}>Off</button>
</>
The second issue is that the function I want to run in the interval (that setnum is standing in for) is actually
function updateUserMoney(){
batch(()=>{
dispatch(updateUser({money: user.money + 1, energy: user.energy - 1}))
dispatch(incrementTime(1))
})
}
Here, the incrementTime function works as intended and continues to increment, but the update user function only fires once.
I think it has the same problem that line 11 has where setnum(num + 1) doesn't work but setnum(num => num + 1) does. I haven't used the second syntax much and don't understand why it's different can anybody tell me?
Here's the full code
import { useState } from "react";
import { batch, useDispatch, useSelector } from "react-redux";
import { incrementTime, updateUser } from "../actions";
const GeneralActions = () => {
const dispatch = useDispatch()
const user = useSelector((state)=>state.user)
const [num, setnum]= useState(0)
let intervalId
function updateUserMoney(){
batch(()=>{
dispatch(updateUser({money: user.money + 1, energy: user.energy - 1}))
dispatch(incrementTime(1))
})
}
function on(){
intervalId = window.setInterval(function(){
updateuserMoney()
setnum(num=>num+1)
}, 400);
}
function off(){
clearInterval(intervalId)
}
return (
<>
<br/>
<>{num}</>
<button onClick={()=>on()}>On</button>
<button onClick={()=>off()}>Off</button>
</>
);
}
export default GeneralActions;
Any insight is appreciated. Thank you!
Every time you set a new state value in React, your component will rerender. When your GeneralActions component rerenders, your entire function runs again:
const GeneralActions = () => {
// code in here runs each render
}
This means things such as intervalId, will be set to undefined when it runs let intervalId; again, and so on this particular render you lose the reference for whatever you may have set it to in the previous render. As a result, when you call off(), it won't be able to refer to the intervalId that you set in your previous render, and so the interval won't be cleared. If you want persistent variables (that aren't related to state), you can use the useRef() hook like so:
const GeneralActions = () => {
const intervalIdRef = useRef();
...
function on(){
clearInterval(intervalIdRef.current); // clear any currently running intervals
intervalIdRef.current = setInterval(function(){
...
}, 400);
}
function off(){
clearInterval(intervalIdRef.current);
}
}
One thing that I've added above is to clear any already created intervals when on() is executed, that way you won't queue multiple. You should also call off() when your component unmounts so that you don't try and update your state when the component no longer exists. This can be done using useEffect() with an empty dependency array:
useEffect(() => {
return () => off();
}, []);
That should sort out the issue relating to being unable to clear your timer.
Your other issue is with regards to setNum() is that you have a closure over the num variable for the setTimeout() callback. As mentioned above, every time your component re-renders, your function body is executed, so all of the variables/functions are declared again, in essence creating different "versions" of the num state each render. The problem you're facing is that when you call setInterval(function(){}), the num variable within function() {} will refer to the num version at the time the function was created/when setInterval() function was called. This means that as you update the num state, your component re-renders, and creates a new num version with the updated value, but the function that you've passed to setInterval() still refers to the old num. So setNum(num + 1) will always add 1 to the old number value. However, when you use useState(num => num + 1), the num variable that you're referring to isn't the num "version"/variable from the surrounding scope of the function you defined, but rather, it is the most up to date version of the num state, which allows you to update num correctly.
useEffect(() => {
playLoop();
}, [state.playStatus]);
const playLoop = () => {
if (state.playStatus) {
setTimeout(() => {
console.log("Playing");
playLoop();
}, 2000);
} else {
console.log("Stopped");
return;
}
};
Output:
Stopped
// State Changed to true
Playing
Playing
Playing
Playing
// State Changed to false
Stopped
Playing // This is the problem, even the state is false this still goes on execute the Truthy stalemate
Playing
Playing
I am working on react-native and I want the recursion to stop when the state value becomes false.
Is there any other way I can implement this code I just want to repeatedly execute a function while the state value is true.
Thank you
Rather than having a playStatus boolean, I'd save the interval ID. That way, instead of setting playStatus to false, call clearInterval. Similarly, instead of setting playStatus to true, call setInterval.
// Can't easily use useState here, because you want
// to be able to call clearInterval on the current interval's ID on unmount
// (and not on re-render) (interval ID can't be in an old state closure)
const intervalIdRef = useRef(-1);
const startLoop = () => {
// make sure this is not called while the prior interval is running
// or first call clearInterval(intervalIdRef.current)
intervalIdRef.current = setInterval(
() => { console.log('Playing'); },
2000
);
};
const stopLoop = () => {
clearInterval(intervalIdRef.current);
};
// When component unmounts, clean up the interval:
useEffect(() => stopLoop, []);
The first thing you should do is make sure to clear the timeout when the state changes to stopped or otherwise check the state within the timeout callback function.
But the problem does not seem to be with the setTimeout code only by itself, but rather that this playLoop is also being called too many times. You should add a console.log with a timestamp right at the start of your playLoop to confirm or disprove this. And to find out where it is called from, you could use console.trace.
const playLoop = () => {
console.log(new Date(), ': playLoop called')
console.trace(); // optional
if (state.playSt....
This is for frontend Javascript linked to an HTML file if that's relevant. I tried using IIFE for this problem and a bunch of things broke, so I'd like to avoid trying again if possible. I declared a timer that I want to stop conditionally (i.e. backend sends front end a message to stop or timer ticks for thirty seconds, whichever comes first), but I'm not sure how to do this without globally declaring a timer variable.
Here's some dummy code because the actual thing is around 300 lines:
const startTimer = ()=>{
let time = 30
const timer = setInterval(()=>{
if(time === 0){
clearInterval(timer)
}
}
},1000)
}
startTimer()
socket.on('stop-timer', ()=>{
//I want to clear the timer when this gets emitted.
})
How would I avoid declaring a global timer? Is it really that bad to do so?
You could create a simple class (I assume you use ES6 by the look of your code) that will expose cancel method to clearInterval like this:
class Timer {
constructor(time) {
this.time = time;
this._interval = null;
}
start() {
this._interval = setInterval(...);
}
cancel() {
clearInterval(this._interval);
this._interval = null;
}
}
const timer = new Timer(30);
timer.start();
socket.on('stop-timer', timer.cancel);
According to documentation the actual delay of setTimeout may be longer than was asked. Could you please point me on documentation or question with answer which explains may the actual delay of setTimeout be shorter than was asked?
Thing is I encountered an issue which happened really seldom and could be explained by such phenomenon. Platforms: Chrome Version 67, NodeJS Version 9.8.0. Also, I am really curious is this statement true for Firefox and other browsers?
You can test a timeout or interval accuracy by taking a delta of two microtimes. In my tests, the timers are plus or minus a few milliseconds. Run the program below to see the results in your own environment.
In a perfect world, the output would always show 1000. The variance in the output means our world is imperfect 😂
var last = Date.now()
var interval = setInterval(function() {
var now = Date.now()
var delta = now - last
console.log(delta)
last = now
}, 1000)
setTimeout(clearInterval, 10000, interval)
// 1000
// 1003
// 998
// 1002
// 999
// 1007
// 1001
// ...
To dramatically affect the result, press Run, switch to another tab, then come back to this tab after a few seconds. You'll see de-focused tabs have extremely high variance.
// 1004 <-- start experiment
// 997
// 1000 <-- switch to another tab
// 1533 <-- variance spikes immediately
// 866
// 1033
// 568 <-- switch back to this tab
// 1001 <-- variance restabilizes
// 1000
// 999
I don't know all of the things that play a role in affecting the accuracy of timeouts and intervals in JavaScript, but I also don't think that's an important thing to know. Ultimately we don't need accuracy because we can calculate precise durations of time using the delta technique above.
a practical example in React
Below we make a simple Timer component which naively uses setInterval to refresh the timer's display once per second...
class Timer extends React.Component {
constructor (props) {
super (props)
this.state = { seconds: 0, timeout: null }
}
componentDidMount () {
this.setState ({
timeout: setInterval (this.tick.bind(this), 1000)
})
}
componentWillUnmount () {
clearTimeout (this.timeout)
}
tick () {
this.setState ({ seconds: this.state.seconds + 1 })
}
render () {
return <div>Naive timer: {this.state.seconds}</div>
}
}
ReactDOM.render
( <Timer />
, document.getElementById ('timer')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="timer"></div>
But due to the unreliable nature of JavaScript's timers, we know that our Timer component will eventually display the incorrect value.
When we implement PreciseTimer below, we can use our delta technique to ensure the component always displays the correct duration
class PreciseTimer extends React.Component {
constructor (props) {
super (props)
this.state = { start: Date.now (), seconds: 0, timeout: null }
}
componentDidMount () {
this.setState ({
timeout: setInterval (this.tick.bind(this), 1000)
})
}
componentWillUnmount () {
clearTimeout (this.timeout)
}
tick () {
const delta = Date.now () - this.state.start
this.setState ({ seconds: delta / 1000 })
}
render () {
return <div>Precise timer: {this.state.seconds}</div>
}
}
ReactDOM.render
( <PreciseTimer />
, document.getElementById ('timer')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="timer"></div>
To see the practical difference in how these two timers behave, start both of them, switch to a new tab for 10-15 seconds, then switch back to this tab. The naive Timer will suffer from JavaScript timer variance whereas PreciseTimer will always display the correct duration.
Actually setTimeout will just push the callback on top of the event loop after the specified delay, but because Javascript is mono process if something block the event loop the callback will be delayed. But it can't be shorter or it's a bug in the code / javascript engine used.
Javascript's single execution thread means that sometimes things that you queue up to be executed in the future at point x, might actually be executed on time x+, since there might be no execution-time-frame available.
However things will never be able to execute BEFORE the required time, only at-or-after your given time
A setTimeout(myFunc,100) for example may execute after 100 ms, or a little bit longer.
You can find more information on this article regarding javascrip's timers in general:https://johnresig.com/blog/how-javascript-timers-work/
To complete user633183's answer, here is a timer that ensures your function never runs before given time interval:
class Timer {
constructor() {
this.last = Date.now();
this.interval = null;
}
start (f, ms) {
// Stop previous timer (if any)
this.stop();
// Start new timer
this.interval = setInterval( () => {
const now = Date.now();
// Don't execute before given time interval
if (now - this.last < ms)
return;
this.last = now;
f();
}, ms);
}
stop () {
if (this.interval)
clearInterval(this.interval);
this.interval = null;
}
}
const timer = new Timer();
timer.start( () => console.log(Date.now()), 1000);