I have a state value that I want to keep track of, declared as such:
const [found, setFound] = useState(0);
A simple increment:
const incrementFound = () => {
setFound(found 1);
}
I call this function in a loop as my code works through an array, as such:
values.forEach((item) => {
doSomething([item]).then(() => {
console.log("Removed " item);
increment();
});
});
And in the return, I render this value like this:
<p>Found {found} item(s).</p>
However, this value will only ever increment once, even if my code has processed multiple elements.
Why is this happening?
CodePudding user response:
setState is an async
method and so while it might appear as though every time you call setFound(found 1)
the value of found
is getting updated to be one more than it was the last time you called the setter, it’s likely that the state update hasn’t yet occurred and every time you call it in your for loop the value of found
remains the same.
You can read more about it in the react docs here: https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
The solution here is to pass a callback method to the setState
method so that when the update happens it can accurately use the previous state value.
setFound(current => current 1)
CodePudding user response:
The entire loop is executed before React updates state.
CodePudding user response:
First, the function you create is called incrementFound
but you calling increment
instead. May you should take a look again.
Second, I'm seeing Promise pattern here, so I assuming that doSomething
do some asynchronus stuff. And this pattern kind of bad IMO. Because when your component rendered, it will calls the forEach, but the forEach in some asynchronus miliseconds later call incrementFound
which will change the state. In React, every time you setState
it's rerender its component. So it will rerender again. But in the function there's a forEach, and goes on. Take a look to this example why this is a bad code. Look at the console, that it will rerender like forever.
const { Fragment, useState, useRef } = React;
function doSomething(data) {
// dummy asynchronus function
return new Promise((res) => setTimeout(res, 200));
}
function App() {
// counting the render
const render = useRef(0);
console.log('rerender ' render.current);
render.current ;
const [found, setFound] = useState(0);
function incrementFound() {
setFound(found 1);
}
Array.from(Array(10)).forEach((_, idx) => {
doSomething().then(() => {
incrementFound();
});
});
return <Fragment>
<h3>check the console</h3>
<div>found {found}</div>
</Fragment>;
}
ReactDOM.createRoot(
document.getElementById('root')
).render(<App/>);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id="root">
loading...
</div>
Solution #1
I think you should wait all of it to waited properly. You could use Promise.all
.
Promise.all(
values.map((item) =>
doSomething([item])
),
);
But, if you setState on iteration again, it will rerender again. So, just put the lenght in it.
Promise.all(
values.map((item) =>
doSomething([item])
),
).then(() => {
setFound(values.length);
});
Solution #2
Use generator-like pattern. What is generator exactly? Well, I doesn't know neither. But AFAIK, it's like separate your iteration by its each step I geuss. So, we could use useEffect
that will watch state change as its dependency. And then, check if the found is less than the values.length
. If it does, do some iteration stuff, like your doSomething
in that. In this method, it will shows the changes slowly not like the solution #1.
function App() {
const [found, setFound] = useState(0);
useEffect(() => {
// imitate forEach iteration
// so, do your iteration here instead
if (found < values.length) {
doSomething([values[found]]).then(() => {
setFound(found 1);
});
}
}, [found]);
}