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"/>
React v18 (apr-2022)
Apparently React v18 batches call from async handlers as well.
https://vogue.globo.com/celebridade/noticia/2022/04/bbb-portugal-bruna-gomes-e-pedida-em-namoro-por-bernardo-sousa.html
