newbie to Javascript & React, trying to learn my way through React by building small projects. I'm working on a countdown timer right now, and I'm wondering if there's a better way of handling the pause/stop logic.
Right now, I'm calling setInterval() when my component is no longer in a "Paused" !paused state, and returning a cleanup() function to clear the interval. When I console logged it, it only cleared interval on pause/start.
However, I want to add functionality to automatically stop the timer once it hits zero, which requires adding duration state as a second dependency. When I logged this to console, I realized that my setDuration and clearInterval was now running at every interval, rather than only on the pause/start (it looks like it's calling setInterval() at every "tick" now.
My question is - is this a problem? And is there a better way to do this?
function Timer(props) {
const [duration, setDuration] = useState(10);
const [paused, setPaused] = useState(true);
useEffect(() => {
let timerId;
if (!paused) {
timerId = setInterval(() => {
setDuration(prev => prev - 1);
}, 1000);
// console.log(timerId);
}
if (duration === 0) {
// console.log(`Time's up`);
clearInterval(timerId);
}
return function cleanup() {
// console.log(`Clearing ${timerId}`);
clearInterval(timerId);
};
}, [paused, duration]);
const handleClick = (e) => {
!paused ? setPaused(true) : setPaused(false);
};
return (
<>
<h3>{duration}</h3>
<Button paused={paused} onClick={handleClick} />
</>
);
}
In your code, duration is changing on every interval and because it is passed as a parameter in dependecy array, it is triggering useEffect on each interval which is causing it to the setDuration and clearInterval to run at each interval.
you can use ternary operator to check if the duration is less than zero then you can automatically set the duration to zero by using setDuration(0)
function Timer(props) {
const [duration, setDuration] = useState(10);
const [paused, setPaused] = useState(true);
useEffect(() => {
let timerId;
if (!paused) {
timerId = setInterval(() => {
setDuration((prev) => prev - 1);
}, 1000);
console.log(timerId);
}
return function cleanup() {
console.log(`Clearing ${timerId}`);
clearInterval(timerId);
};
}, [paused]);
const handleClick = (e) => {
!paused ? setPaused(true) : setPaused(false);
};
return (
<>
{duration >= 0 ? <h3>{duration}</h3> : setDuration(0)}
<button paused={paused} onClick={handleClick} />
</>
);
}
Play around with the code here
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.
Need help to understand in useEffect, if I don't put counter and timerCheck in useEffect dependency then what it will effect here.
And if I put timerCheck dependency in useEffect then counter increasing 100 time faster
Also how can i run this code without any error or warning
code in codesandbox
const [counter, setCounter] = useState(0);
const [startcounter, setStartcounter] = useState(false);
const [timerCheck, setTimerCheck] = useState(0);
useEffect(() => {
let intervalStart = null;
if (startcounter) {
intervalStart = setInterval(() => {
setCounter((prevState) => (prevState += 1));
}, 1000);
setTimerCheck(intervalStart);
} else {
clearInterval(timerCheck);
}
return () => {
if (counter !== 0) clearInterval(timerCheck);
};
}, [startcounter]);
const handleStartButton = () => {
setStartcounter(true);
};
const handleStopButton = () => {
setStartcounter(false);
};
Try not to declare unnecessary states. timerCheck state is redundant.
You want to start an interval that increases counter by one every second.
When user stops the stopwatch , you want the interval to be cleared, so it would stop increasing the timer.
So your only dependancy in useEffect would be whether your stopwatch is running or not. And the only thing that matters to you, is when startCounter is ture. You start an interval when it is true and clear the interval in its cleanup.
useEffect(() => {
let intervalStart = null;
if (!startcounter) {
return () => {};
}
intervalStart = setInterval(() => {
setCounter((prevState) => (prevState += 1));
}, 1000);
return () => {
clearInterval(intervalStart);
};
}, [startcounter]);
Here is the codesandbox
But you do not really need useEffect for this. IMO it would be a much better code without using useEffect.
So useEffect with no dependency will run on every render.
If you include a dependency, it will re-render and run whenever that dependency changes. So by including timerCheck in it, you're telling the page to re-render whenever that changes, which ends up changing it, causing it to re-render again, causing it to change again, etc., etc.
More info: https://www.w3schools.com/react/react_useeffect.asp
I'm building a stopwatch UI that shows the time in seconds. With the click of a button, the timer will start counting upwards and stop when it is clicked again. User should be able to start it again.
The issue I'm having is that I can have setInterval working correctly but once I include setTime hook, the component updates to render the time in the UI but the setInterval instance is being called multiple times. This leads to odd rendering behavior.
const Timer = () => {
const [time, setTime] = useState(0)
let timer
const startStopTimer = () => {
if (!timer) timer = setInterval(() => setTime(time++), 1000)
else {
clearInterval(timer)
timer = null
}
}
return (
<div>
<p>Time: {time} seconds</p>
<Button
onClick={() => {
startStopTimer()
}
> Start/Stop </Button>
</div>
)
}
Example behavior would be:
User clicks Start/Stop
Timer starts from 0 and counts upward
User clicks Start/Stop
Timer stops immediately
User clicks Start/Stop
Timer continues where it left off
This is a classic example of stale closure in React hooks, inside your setInterval value of time is not changing after calling setTime. Change your code with:
setInterval(() => setTime(currentTime => currentTime + 1), 1000).
setTime just like the setState of classful components also accepts a callback function which has the current value as the first param
Also, the timer variable is useless in you code since on every re-render it will be undefined and you wont't have the access of return value of setInterval, so it will reinitialize the setInterval. To handle that use useRef, you can store the return of setInterval in .current, which will be available to you after subsequent re renders so no more re-init of setInterval and you can also use clearInterval
Solution:
const {useState, useRef} = React;
const {render} = ReactDOM;
const Timer = () => {
const [time, setTime] = useState(0);
const timer = useRef(null);
const startStopTimer = () => {
if (!timer.current) {
timer.current = setInterval(() => setTime(currentTime => currentTime + 1), 1000);
} else {
clearInterval(timer.current);
timer.current = null;
}
};
return (
<div>
<p>Time: {time} seconds</p>
<button
onClick={startStopTimer}
>
Start/Stop
</button>
</div>
);
};
render(<Timer />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Here is an example using a react class component. This example keeps track of the start time instead of adding to some value on a certain interval. Then when you stop the timer it accumulates the passed time.
The callback passed to setInterval might not always exactly be called each n ms. If the JavaScript engine is busy it might take a few ms longer. Keeping a counter would slowly offset the actual passed time the longer it runs.
const {Component} = React;
const {render} = ReactDOM;
class StopWatch extends Component {
state = {startTime: null, accTime: 0, intervalId: null};
componentWillUnmount() {
clearInterval(this.state.intervalId);
}
ms() {
const {startTime, accTime} = this.state;
if (!startTime) return accTime;
return Date.now() - startTime + accTime;
}
start = () => {
this.setState({
startTime: Date.now(),
intervalId: setInterval(() => this.forceUpdate(), 10)
});
}
stop = () => {
clearInterval(this.state.intervalId);
this.setState({
startTime: null,
accTime: this.ms(),
intervalId: null
});
}
reset = () => {
this.setState({
accTime: 0,
startTime: this.state.startTime && Date.now()
});
}
render() {
return (
<div>
<h1>{this.ms() / 1000}</h1>
{this.state.startTime
? <button onClick={this.stop}>stop</button>
: <button onClick={this.start}>start</button>}
<button onClick={this.reset}>reset</button>
</div>
);
}
}
render(<StopWatch />, document.getElementById("stop-watch"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="stop-watch"></div>
I'm creating a stopwatch that has a button to start and stop the time, but I'm having trouble with setInterval behavior.
When declared in React's functional component level, it will run once it's mounted.
Example:
const Timer = () => {
const timer = setInterval(() => console.log('running'), 1000)
}
When I declare it inside a function, it will not run until the function is called but then I can't get it to stop.
const Timer = () => {
const [start, setStart] = useState(false)
const startStopTimer = () => {
const timer = setInterval(() => console.log('running'), 1000)
}
return (<Button
onClick={() => {
setStarted(!start)
startStopTimer()
}
> Start/Stop </Button>)
}
I've then tried adding clearInterval() in the function and calling it conditionally if start === false. In this implementation, the first button click does nothing. Second click starts the timer, but it can't be stopped.
const Timer = () => {
const [start, setStart] = useState(false)
const startStopTimer = () => {
let timer = setInterval(() => console.log('running'), 1000)
if (!started) clearInterval(timer)
}
return (<Button
onClick={() => {
setStarted(!start)
startStopTimer()
}
> Start/Stop </Button>)
}
Ciao, I suggest you to modify your code like this:
const Timer = () => {
//const [start, setStart] = useState(false) do not use state for scripting reasons
let timer = null;
const startStopTimer = () => {
if(!timer) timer = setInterval(() => console.log('running'), 1000)
else {
clearInterval(timer)
timer = null
}
}
return (<Button
onClick={() => {
//setStarted(!start)
startStopTimer()
}
> Start/Stop </Button>)
}
Explanation: timer should be defined outside the startStopTimer otherwise, every time you launch startStopTimer, you create a new timer.
Ok this was easy, but now the important part: I strongly suggest you to not use react state for scripting reason. react state should be used only for rendering reasons. Why? Because hook are async and if you read start immediately after use setStart, you will read an old value.
timer variable is local and only scoped inside sartStopTimer() every time you call sartStopTimer() it generates new timer and when clearing timeout you are only clearing the recently generated one not previous timer
let timer;
const startStopTimer = () => {
timer = setInterval(() => console.log('running'), 1000)
if (!started) clearInterval(timer)
}
Made timer global variable.
This should get you started.
Try this:
const Timer = () => {
const [timerId, setTimerId] = useState(null)
function startStopTimer(){
if (timerId) {
clearInterval(timerId)
setTimerId(null)
} else {
setTimerId(setInterval(() => console.log('hello'), 1000))
}
}
return <button onClick={startStopTimer}> Start/Stop </button>
}
There is no need to use state, by the way. In case you do not need to keep the timer state you can definitely use plain variable, of course.
I'm making a simple progress bar and I've noticed strange behaviour of hook state when I'm using setInterval. Here is my sample code:
const {useState} = React;
const Example = ({title}) => {
const [count, setCount] = useState(0);
const [countInterval, setCountInterval] = useState(0)
let intervalID
const handleCount = () => {
setCount(count + 1)
console.log(count)
}
const progress = () => {
intervalID = setInterval(() => {
setCountInterval(countInterval => countInterval + 1)
console.log(countInterval)
if(countInterval > 100) { // this is never reached
setCountInterval(0)
clearInterval(intervalID)
}
},100)
}
const stopInterval = () => {
clearInterval(intervalID)
}
return (
<div>
<p>{title}</p>
<p>You clicked {count} times</p>
<p>setInterval count { countInterval } times</p>
<button onClick={handleCount}>
Click me
</button>
<button onClick={progress}>
Run interval
</button>
<button onClick={stopInterval}>
Stop interval
</button>
</div>
);
};
// Render it
ReactDOM.render(
<Example title="Example using simple hook:" />,
document.getElementById("app")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="app"></div>
If I set state by handleCount everything is happen as expected, but when I'm running progress function, inside setInterval countInterval value doesn't change at all. Regardless of it, countInterval has changed in the state.
To get this around I'm using variable inside progress function, like this:
const progress = () => {
let internalValue = 0
intervalID = setInterval(() => {
setCountInterval(internalValue)
internalValue++
if(internalValue > 100) {
setCountInterval(0)
clearInterval(intervalID)
}
},100)
}
And that works fine, but I'm still wondering if there is a better approach and what I'm doing wrong in the first case.
The second problem is that I can't clear interval outside a progress function and I'm not sure what I'm doing wrong here or I miss something? Thanks in advance for any help and advices.
Your problems are caused by your timer references being lost on re-renders, and the setInterval call-back referencing out-of-date versions of setCountInterval.
To get it fully working, I would suggest adding a state variable to track whether it is started or not and a useEffect to handle the setting and clearing of setInterval.
const Example = ({ title }) => {
const [count, setCount] = useState(0);
const [countInterval, setCountInterval] = useState(0);
const [started, setStarted] = useState(false);
const handleCount = () => {
setCount(count + 1);
console.log(count);
};
useEffect(() => {
let intervalID;
if (started) {
intervalID = setInterval(() => {
setCountInterval(countInterval => countInterval + 1);
console.log(countInterval);
if (countInterval > 100) {
setCountInterval(0);
setStarted(false);
}
}, 100);
} else {
clearInterval(intervalID);
}
return () => clearInterval(intervalID);
}, [started, countInterval]);
return (
<div>
<p>{title}</p>
<p>You clicked {count} times</p>
<p>setInterval count {countInterval} times</p>
<button onClick={handleCount}>Click me</button>
<button onClick={() => setStarted(true)}>Run interval</button>
<button onClick={() => setStarted(false)}>Stop interval</button>
</div>
);
};
Working sandbox here: https://codesandbox.io/s/elegant-leaf-h03mi
countInterval is a local variable, that contains a primitive value. By the nature of JavaScript, there is no way for setState to mutate that variable in any way. Instead, it reexecutes the whole Example function, and then useState will return the new updated value. That's why you can't access the updated state, until the component rerenders.
Your problem can trivially be solved by moving the condition into the state update callback:
setCountInterval(countInterval => countInterval > 100 ? 0 : countInterval + 1)
Because of the rerendering, you can't use local variables, so intervalId will be recreated at every rerender (and it's value gets lost). Use useRef to use values across rerenders.
const interval = useRef(undefined);
const [count, setCount] = useState(0);
function stop() {
if(!interval.current) return;
clearInterval(interval.current);
interval.current = null;
}
function start() {
if(!interval.current) interval.current = setInterval(() => {
setCount(count => count > 100 ? 0 : count + 1);
});
}
useEffect(() => {
if(count >= 100) stop();
}, [count]);