Home > Software engineering >  What happens when setState is invoked?
What happens when setState is invoked?

Time:04-03

I am learning React and having difficulty in understanding the flow of code that is happening.

Here is my code:It is a functional React component

function App() {
  const [x, setx] = useState(1);
  const [x1, setx1] = useState(1);
  const [x2, setx2] = useState(1);
  const [x3, setx3] = useState(1);
  const [x4, setx4] = useState(1);
  const [x5, setx5] = useState(1);

  console.log("out3");

  const bclick = () => {
    setx1(x1   1);
    setx2(x2   1);
    setx3(x3   1);
    setx4(x4   1);
    setx5(x5   1);
    setx(x   1);
    console.log("out1");
    bclick2();
  };
  const bclick2 = () => {
    console.log("out2");
  };
  console.log("out4");

  return (
    <div className="App">
      {console.log("in")}
      <button onClick={bclick} />
    </div>
  );
}

output of console.log() after clicking on button: out1 out2 out3 out4 in


Q> Upon clicking on button multiple different setStates are executed. Will they re-evaluate the component or the function chain(bclick and bclick2) complete executing and then App component is re-evaluated. Based on my output I realise that function chain is executed first. So is this how setState works? Will flow of code complete first (irrespective of number of functions) and then functional component re-evaluated?

CodePudding user response:

This has to do with React batching setState calls to optmize the number of renders. Usually you don't have to worry about that.

The setState call is async. React will decide when and how to apply multiple setState calls.

The handlers will always finish running before React re-renders. That's why you are seeing the bclick2() call running before any re-renders.

I feel that React will always go for batching multiple setState calls in a single re-render. But you can see that if you wrap multiple setState calls in setTimeout, React will re-render multiple times, because there's no way of it to know how long those timeouts will take to complete. You might be calling an API, for example.

function App() {

  console.log('Rendering App...');

  const [x, setx] = React.useState(1);
  const [x1, setx1] = React.useState(1);
  const [x2, setx2] = React.useState(1);
  const [x3, setx3] = React.useState(1);
  const [x4, setx4] = React.useState(1);
  const [x5, setx5] = React.useState(1);

  const bclick = () => {
    console.clear();
    console.log("From bclick (batched: single render)");
    setx1(x1   1);
    setx2(x2   1);
    setx3(x3   1);
    setx4(x4   1);
    setx5(x5   1);
    setx(x   1);
    console.log("Calling bclick2");
    bclick2();
  };

  const bclick2 = () => {
    console.log("From bclick2");
  };
  
  const notBatched = () => {
    console.clear();
    console.log('From notBatched (multiple renders)');
    setTimeout(() => setx1((prevState) => prevState 1),0);
    setTimeout(() => setx1((prevState) => prevState 1),0);
    setTimeout(() => setx1((prevState) => prevState 1),0);
  };
  
  

  return (
    <div className="App">
      <button onClick={bclick}>Click (will batch)</button>
      <button onClick={notBatched}>Click (will not batch)</button>
    </div>
  );
}

ReactDOM.render(<App/>,document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>

For example, if you are calling an API from a useEffect:

useEffect(() => {
  setIsLoading(true);             // 1
  const data = await fetchAPI();  
  setData(data);                  // 2
  setIsLoading(false);            // 3
},[]);

In this case React wil run #1, and then, when the API call completes, it will run #2 and #3 separately (not batched). Not sure why it chooses to do it separately, because it would be safe to run them together, but I'm sure React has its own reasons for that. Probably the whole block that has been timeout'd because of the API call is flagged to shouldNotBatch somehow. I don't actually know what is the internal logic they use for this.

const fetchAPI = () => {
  return new Promise((resolve) => {
    setTimeout(() => resolve('DATA'),1500);
  });
}

const App = () => {
  console.log('Rendering App...');
  const [isLoading,setIsLoading] = React.useState(false);
  const [data,setData] = React.useState(null);

  // I changed the `await` to `then` because SO snippets don't allow `await` in this case
  React.useEffect(() => {
    console.log('Running useEffect...');
    setIsLoading(true);             // 1
    fetchAPI().then((data) => {
      setData(data);                // 2
      setIsLoading(false);          // 3
    });;  
  },[]);

  return(
    <div>
      <div>isLoading:{JSON.stringify(isLoading)}</div>
      <div>data:{JSON.stringify(data)}</div>
    </div>
  );
};

ReactDOM.render(<App/>, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>

  • Related