State doesn't update inside setInterval - javascript

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

Related

Best way to use setInterval inside React useEffect in countdown timer

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

The changes made to state variable in my react app is not updated to sub function calls

const [ change , setChange ] = useState(0);
const funct = () => {
if (change === 100){
return 0;
}
setChange((pre) => {
return pre + 10
});
funct()
}
if I call the function fuct() when any event occur , it will have to call itself (recursive function) until the value of change become 100. But here the function is running infinitely(infinte recursion). This is because the state variable is not changing at every instant of setChange() call.
WHY ?
WHY DID THE STATE IS NOT CHANGED BETWEEN RECURSIVE FUNCTION CALLS ?
WHY ? WHY DID THE STATE IS NOT CHANGED BETWEEN RECURSIVE FUNCTION CALLS ?
Since funct() is triggered via React-based event, state updates are batched.
In order not to batch state update, the trigger should come outside of React-based events, like setInterval().
With that said, here's an example using useEffect() hook with setInterval().
const {useState, useEffect} = React;
const App = (props) => {
const [change, setChange] = useState(0);
useEffect(() => {
const t = setInterval(() => (
setChange((change) => (change + 10))
), 1000);
return () => clearInterval(t);
}, [change]);
return (
<div>{`Change: ${change}`}</div>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<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="root"></div>
This is because the dispatch function of useState (setChange in this case) is a async operation and doesn't update the state as soon as it is called. You can try this code instead:
const [change, setChange] = useState(0);
const funct = (pre = 0) => {
if (pre === 100) {
return 0;
}
setChange(pre + 10);
funct(pre + 10);
}
Here, the use of setChange can be avoided but, considering your original code might required that, I have provided the solution.
Please make a note! The state will never be useful until the line of codes is left to process. you cannot use useState in that way:
const [mystate, setState] = useState(0);
const getSum = () => {
setState(10);
sum = mystate + 10 //And expect 20? No, you will never get 20 but you will receive 10.
}
always complete your code using let or var and send back the results to the state.
const [ change , setChange ] = useState(0);
let iter = 0;
const funct = () => {
if (iter === 100){
setChange(iter);
}
else{
iter=iter+10
funct();
}
}
You cannot use "change" to return the count value every time the function runs but you can use "iter"! In the end you will have change with total values.
setState/usestate are Asynchronous functions i.e. we can't setState on one line and assume state has changed on the next.
You can try this:
useEffect(() => {
if(change!==0){
funct();
}
}, [change])
const funct = () => {
if (change === 100){
return 0;
}
setChange((pre) => {
return pre + 10
});
}

setInterval + React hooks causing multiple updates within component

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>

setInterval start and stop behavior

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.

React hook setState arguments

I would like to know the difference between the following two versions of code. Both versions do the same.
1) Here just the counter variable is used to get the current value
const Counter = () => {
const [counter, setCounter] = useState(0);
return <button onClick={() => setCounter(counter + 1)}>{counter}</button>;
}
2) This version passes a function to setCounter
const Counter = () => {
const [counter, setCounter] = useState(0);
return <button onClick={() => setCounter(c => c + 1)}>{counter}</button>;
}
The official documentation says:
If the new state is computed using the previous state, you can pass a
function to setState. The function will receive the previous value,
and return an updated value.
So what's wrong with the first option? Are there some pitfalls?
With the particular code in your example, you have the previous value in hand, so there isn't much difference. But sometimes you don't. For example, suppose you wanted to have a memoized callback function. Due to the memoization, the value of counter gets locked in when the closure is created, and won't be up to date.
const Counter = () => {
const [counter, setCounter] = useState(0);
// The following function gets created just once, the first time Counter renders.
const onClick = useCallback(() => {
setCounter(c => c + 1); // This works as intended
//setCounter(counter + 1); // This would always set it to 1, never to 2 or more.
}, []); // You could of course add counter to this array, but then you don't get as much benefit from the memoization.
return <button onClick={onClick}>{counter}</button>;
}

Categories

Resources