Home > Back-end >  React updating state does not change render
React updating state does not change render

Time:01-04

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]);
}
  • Related