Infinity loop using useState with useEffect in React - reactjs

I want to increase the state [second] = [second + 1] after every second.
const [second,setSecond] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSecond(second+1)
}, 1000);
return () => clearInterval(interval);
}, []);
But it seems that an infinity loop occurs, the [second] just increases once, from 0 to 1, and it stops running.
I changed my code from
setSecond(second+1)
to
setSecond((second) => {return second+1})
And this one runs without problem:
const [second,setSecond] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSecond((second) => {return second+1})
}, 1000);
return () => clearInterval(interval);
}, []);
Seriously, I still don't get it clearly. Can anybody explain to me why? Thanks in advance!

Looking at your code, I think the issue is with the value of second being bound to 0.
const [second,setSecond] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSecond(second+1)
}, 1000);
return () => clearInterval(interval);
}, []);
Here when your component mounts, the useEffect is executed. At that time you are creating a function and passing it to setInterval. This function will use the value of second at the time of creation (which equals 0). So everytime when the setInterval runs, it is executing setSecond(0+1) which always equals 1.
The correct way that you have mentioned works because you're giving it a function which gets passed into it the current value of state everytime it is executed.

Ciao, the 2 ways you are using to set second are different.
This way:
setSecond(second+1)
does not consider the previuos value of second. It just try to increment second by 1 by reading current value of second. Considering that setSecond is async, on next setInterval is not guaranteed that second will be updated by the previous setSecond. So in this way you could have glitch.
This way:
setSecond((second) => {return second+1})
is the correct one. Here you are considering the previuos value of second (by using arrow function). So in this case, second will be correctly update.
You could make a test: Take a button and on onclick function try to write:
setSecond(second+1)
setSecond(second+1)
you will see that second will be incremented by 1 (and not by 2 as expected).
Now modify your code like this:
setSecond((second) => {return second+1})
setSecond((second) => {return second+1})
you will see that second will be incremented by 2!
This happends because Hooks are async.

Related

useState hook - passing in callback vs state value

In this very simple click counter example:
const App = () => {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
}
return <button onClick={handleClick}>{count}</button>
}
My understanding of the flow is:
Component gets mounted with count=0
When button is clicked, new count state is set with increment of 1.
That triggers a re-render, so now I have count=1, and repeats.
However, using the same understanding, why does it not work here?
const App = () => {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1)
}, 500)
return () => {
clearInterval(timer)
}
}, [])
console.log(count) // this stops at 1, so the timer stops triggering??
return <h1>{count}</h1> // get stuck at 1
}
The outcome of the above code is that the count gets stuck at value 1. (and weirdly the console.log stops too).
I thought each time the setInterval timer triggers, the count increment will cause a re-render with new count value and therefore it will just increase by 1 forever?
The fix here is to simply pass in a function argument to access prevState:
const timer = setInterval(() => {
setCount(oldCount => oldCount + 1)
}, 500)
But why couldn't the first approach work?
Hope someone can point me to a good article or documentation of this, I tried searching around but couldn't get any explanation.
The first approach doesn't work because with this
setCount(count + 1)
You are creating a copy of the count value at that particular time you created the callback. This means that every 500ms you will re-execute this line
setCount(0 + 1)
That won't cause a re-render because react is intelligent enough to understand that you are passing the same value to the setCount function so the re-render would not be necessary.
However by passing a callback to setCount:
setCount(oldCount => oldCount + 1)
You are saying that you want the current value of the state count, so each time the argument of that function will be different and therefore it will cause a re-render.
You can find a doc about this topic here: https://reactjs.org/docs/hooks-reference.html#functional-updates

State not update when using setter and setInterval in useEffect

I have the following code, which I expect it to print an extra 10 every 2 seconds on the screen. However, it only prints 10 once. console.log(digits) gave me the correct array of 10s. How can I add a 10 to the array every 2 seconds and print the updated array on the screen each time the 10 is added?
Code sandbox here: https://codesandbox.io/s/silly-https-zk58p?file=/src/App.js
import { useEffect, useState } from "react";
let data = [];
export default function App() {
const [digits, setDigits] = useState([]);
useEffect(() => {
let timer = setInterval(() => {
data.push(10);
setDigits(data);
console.log(digits);
}, 2000);
return () => clearInterval(timer);
}, [digits]);
return <div className="App">{digits}</div>;
}
The issue is with setDigits(data). With arrays, you should be executing setDigits([...data]).
Another way would be doing this:
let timer = setInterval(() => {
setDigits([...digits, 10]);
}, 2000);
Whenever dealing with objects, you should treat them as immutables. What happened here is you modifying an array and puhsing the SAME array into a state. While the value might be different, the array is actually the same hence it does not update the render. Whenever doing [...data] it creates a new array with the same values hence trigger the update.
useEffect picks up the new value change hence why it fires again(cant be observed by console.log()), but this does not trigger re-render of the component.
In your code, you are mutating the same array data by pushing 10 after each 2 seconds. As a result, the useEffect is not executing in the subsequent renders. So either you spread data array as in the following code snippet or simply get rid of the data variable and rely on digits array as Lith suggested.
export default function App() {
const [digits, setDigits] = useState([]);
useEffect(() => {
let timer = setInterval(() => {
data = [...data, 10];
setDigits(data);
console.log(digits);
}, 2000);
return () => clearInterval(timer);
}, [digits]);
return <div className="App">{digits}</div>;
}

React state not updating inside setInterval

I'm trying to learn React with some simple projects and can't seem to get my head around the following code, so would appreciate an explanation.
This snippet from a simple countdown function works fine; however, when I console.log, the setTime appears to correctly update the value of 'seconds', but when I console.log(time) immediately after it gives me the original value of 3. Why is this?
Bonus question - when the function startCountdown is called there is a delay in the correct time values appearing in my JSX, which I assume is down to the variable 'seconds' being populated and the start of the setInterval function, so I don't get a smooth and accurate start to the countdown. Is there a way around this?
const [ time, setTime ] = useState(3);
const [ clockActive, setClockActive ] = useState(false);
function startCountdown() {
let seconds = time * 60;
setClockActive(true);
let interval = setInterval(() => {
setTime(seconds--);
console.log(seconds); // Returns 179
console.log(time); // Returns 3
if(seconds < 0 ) {
clearInterval(interval);
}
}, 1000)
};
Update:
The reason you are not seeing the correct value in your function is the way that setState happens(setTime). When you call setState, it batches the calls and performs them when it wants to in the background. So you cannot call setState then immediately expect to be able to use its value inside of the function.
You can Take the console.log out of the function and put it in the render method and you will see the correct value.
Or you can try useEffect like this.
//This means that anytime you use setTime and the component is updated, print the current value of time. Only do this when time changes.
useEffect(()=>{
console.log(time);
},[time]);
Every time you setState you are rerendering the component which causes a havoc on state. So every second inside of your setInterval, you are re-rendering the component and starting it all over again ontop of what you already having running. To fix this, you need to use useEffect and pass in the state variables that you are using. I did an example for you here:
https://codesandbox.io/s/jolly-keller-qfwmx?file=/src/clock.js
import React, { useState, useEffect } from "react";
const Clock = (props) => {
const [time, setTime] = useState(3);
const [clockActive, setClockActive] = useState(false);
useEffect(() => {
let seconds = 60;
setClockActive(true);
const interval = setInterval(() => {
setTime((time) => time - 1);
}, 1000);
if (time <= 0) {
setClockActive(false);
clearInterval(interval);
}
return () => {
setClockActive(false);
clearInterval(interval);
};
}, [time, clockActive]);
return (
<>
{`Clock is currently ${clockActive === true ? "Active" : "Not Active"}`}
<br />
{`Time is ${time}`}
</>
);
};
export default Clock;

How to map an array , and change the state asynchrony using react-hooks

I want a component to re-render every 5sec and displaying the array at the corresponding indexes, the array length is known to be exactly 10.
here is what I did so far :
const GameCard = ({ gameArray, startGame }) => {
const [arrayIndex, setArrayIndex] = useState(0);
let {questionWord} = gameArray[arrayIndex] ;
useEffect(() => {
if(!startGame) return;
let timer = setTimeout(() => {
if(arrayIndex === 9) return; //is this valid ?
setArrayIndex(arrayIndex +1)} , 1500)
return () => {
clearTimeout(timer)
}
}, [arrayIndex ,startGame]);
return (<div>{questionWord}</div>)
startGame is a boolean prop that gets changed on user click.
Now, this is working but as you can see im triggering the useEffect base on 2 variables, and when i reach the end of the array I'm returning inside setTimeout preventing the arrayIndex from updating.
This feels hacky, how can I improve my async useEffect?
and what happens when we return from setTimeout function , or useEffect?
It might be tempting to use functional updates like setArrayIndex((i) => i + 1):
If the new state is computed using the previous state, you can pass a function to setState.
However, your code needs to use the original value of the arrayIndex constant, i.e. if it was 0 by the time setTimeout was registered, you want it to be 0 even after 5 seconds.
This is the case already in your code - it will be different inside different renders (each timer will have different value because it was registered inside a different render), but the value will never change between registration and execution of a timer (see Closures).
As for improvements, it is possible to detect the arrayIndex === 9 even before registering a new setTimeout like this:
useEffect(() => {
if(!startGame || arrayIndex >= 9) return
const timer = setTimeout(() => {
setArrayIndex(arrayIndex + 1)
}, 5000)
return () => clearTimeout(timer)
}, [arrayIndex, startGame])
Moreover, if you want the timer to discount the render time (i.e. not 5 seconds AFTER each render, but 5 second intervals), you will need a mutable reference instead of an immutable state:
const arrayIndexRef = useRef(0)
const arrayIndex = arrayIndexRef.current
useEffect(() => {
if(!startGame) return
const timer = setInterval(() => {
arrayIndexRef.current += 1
if (arrayIndexRef.current >= 9) {
clearInterval(timer)
}
}, 5000)
return () => clearInterval(timer)
}, [startGame])

react hooks and setInterval

Is there any alternative to just keeping a "clock" in the background to implement auto-next (after a few seconds) in carousel using react hooks?
The custom react hook below implements a state for a carousel that supports manual (next, prev, reset) and automatic (start, stop) methods for changing the carousel's current (active) index.
const useCarousel = (items = []) => {
const [current, setCurrent] = useState(
items && items.length > 0 ? 0 : undefined
);
const [auto, setAuto] = useState(false);
const next = () => setCurrent((current + 1) % items.length);
const prev = () => setCurrent(current ? current - 1 : items.length - 1);
const reset = () => setCurrent(0);
const start = _ => setAuto(true);
const stop = _ => setAuto(false);
useEffect(() => {
const interval = setInterval(_ => {
if (auto) {
next();
} else {
// do nothing
}
}, 3000);
return _ => clearInterval(interval);
});
return {
current,
next,
prev,
reset,
start,
stop
};
};
There are differences between setInterval and setTimeout that you may not want to lose by always restarting your timer when the component re-renders. This fiddle shows the difference in drift between the two when other code is also running. (On older browsers/machines—like from when I originally answered this question—you don't even need to simulate a large calculation to see a significant drift begin to occur after only a few seconds.)
Referring now to your answer, Marco, the use of setInterval is totally lost because effects without conditions dispose and re-run every time the component re-renders. So in your first example, the use of the current dependency causes that effect to dispose and re-run every time the current changes (every time the interval runs). The second one does the same thing, but actually every time any state changes (causing a re-render), which could lead to some unexpected behavior. The only reason that one works is because next() causes a state change.
Considering the fact that you are probably not concerned with exact timing, is is cleanest to use setTimeout in a simple fashion, using the current and auto vars as dependencies. So to re-state part of your answer, do this:
useEffect(
() => {
if (!auto) return;
const interval = setTimeout(_ => {
next();
}, autoInterval);
return _ => clearTimeout(interval);
},
[auto, current]
);
Generically, for those just reading this answer and want a way to do a simple timer, here is a version that doesn't take into account the OP's original code, nor their need for a way to start and stop the timer independently:
const [counter, setCounter] = useState(0);
useEffect(
() => {
const id= setTimeout(() => {
setCounter(counter + 1);
// You could also do `setCounter((count) => count + 1)` instead.
// If you did that, then you wouldn't need the dependency
// array argument to this `useEffect` call.
}, 1000);
return () => {
clearTimeout(id);
};
},
[counter],
);
However, you may be wondering how to use a more exact interval, given the fact that setTimeout can drift more than setInterval. Here is one method, again, generic without using the OP's code:
// Using refs:
const [counter, setCounter] = useState(30);
const r = useRef(null);
r.current = { counter, setCounter };
useEffect(
() => {
const id = setInterval(() => {
r.current.setCounter(r.current.counter + 1);
}, 1000);
return () => {
clearInterval(id);
};
},
[] // empty dependency array
);
// Using the function version of `setCounter` is cleaner:
const [counter, setCounter] = useState(30);
useEffect(
() => {
const id = setInterval(() => {
setCounter((count) => count + 1);
}, 1000);
return () => {
clearInterval(id);
};
},
[] // empty dependency array
);
Here is what is going on above:
(first example, using refs): To get setInterval's callback to always refer to the currently acceptable version of setCounter we need some mutable state. React gives us this with useRef. The useRef function will return an object that has a current property. We can then set that property (which will happen every time the component re-renders) to the current versions of counter and setCounter.
(second example, using functional setCounter): Same idea as the first, except that when we use the function version of setCounter, we will have access to the current version of the count as the first argument to the function. No need to use a ref to keep things up to date.
(both examples, continued): Then, to keep the interval from being disposed of on each render, we add an empty dependency array as the second argument to useEffect. The interval will still be cleared when the component is unmounted.
Note: I used to like using ["once"] as my dependency array to indicate that I am forcing this effect to be set up only once. It was nice for readability at the time, but I no longer use it for two reasons. First, hooks are more widely understood these days and we have seen the empty array all over the place. Second, it clashes with the very popular "rule of hooks" linter which is quite strict about what goes in the dependency array.
So applying what we know to the OP's original question, you could use setInterval for a less-likely-to-drift slideshow like this:
// ... OP's implementation code including `autoInterval`,
// `auto`, and `next` goes above here ...
const r = useRef(null);
r.current = { next };
useEffect(
() => {
if (!auto) return;
const id = setInterval(() => {
r.current.next();
}, autoInterval);
return () => {
clearInterval(id);
};
},
[auto]
);
Because the current value is going to change on every "interval" as long as it should be running, then your code will start and stop a new timer on every render. You can see this in action here:
https://codesandbox.io/s/03xkkyj19w
You can change setInterval to be setTimeout and you will get the exact same behaviour. setTimeout is not a persistent clock, but it doesn't matter since they both get cleaned up anyways.
If you do not want to start any timer at all, then put the condition before setInterval not inside of it.
useEffect(
() => {
let id;
if (run) {
id = setInterval(() => {
setValue(value + 1)
}, 1000);
}
return () => {
if (id) {
alert(id) // notice this runs on every render and is different every time
clearInterval(id);
}
};
}
);
So far, it seems that both solutions below work as desired:
Conditionally creating timer — it requires that useEffect is dependent both on auto and current to work
useEffect(
() => {
if (!auto) return;
const interval = setInterval(_ => {
next();
}, autoInterval);
return _ => clearInterval(interval);
},
[auto, current]
);
Conditionally executing update to state — it does not require useEffect dependencies
useEffect(() => {
const interval = setInterval(_ => {
if (auto) {
next();
} else {
// do nothing
}
}, autoInterval);
return _ => clearInterval(interval);
});
Both solutions work if we replace setInterval by setTimeout
You could use useTimeout hook that returns true after specified number of milliseconds.
https://github.com/streamich/react-use/blob/master/docs/useTimeout.md

Resources