As far as I know, React 18 will auto-batch state updates to reduce re-render (including updates inside of timeouts, promises...), e.g.
function handleClick() {
setCount(c => c + 1); // Does not re-render yet
setFlag(f => !f); // Does not re-render yet
// React will only re-render once at the end (that's batching!)
}
fetch(/*...*/).then(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React will only re-render once at the end (that's batching!)
})
but batch will not applies to different timeouts/promises, e.g.
const fetchData = async () => {
const resA = await fetch('...').then(res => res.json()).then(data => data);
setA(resA);
const resB = await fetch('...').then(res => res.json()).then(data => data);
setB(resB);
// this will end up having two re-renders
}
This is why I usually intentionally batch state updates like this:
const fetchData = async () => {
const resA = await fetch('...').then(res => res.json()).then(data => data);
const resB = await fetch('...').then(res => res.json()).then(data => data);
setA(resA);
setB(resB);
// this works, only one re-render
}
but I recently came upon this inconsistent batching behavior between React event and useEffect, here's the experiment code:codesandbox Note:strictMode is commented out.
// ContextProvider.js
const initialState = {
a: null,
b: null,
c: null,
d: null,
e: null
}
const testReducer = (state, action) => {
switch(action.type) {
case 'capitalA' : {
return {
...state, a: "A"
}
}
case 'capitalB' : {
return {
...state, b: "B"
}
}
case 'capitalC' : {
return {
...state, c: "C"
}
}
case 'capitalD' : {
return {
...state, d: "D"
}
}
case 'capitalE' : {
return {
...state, e: "E"
}
}
default : {
return state;
}
}
}
// Test.js
export default function Test() {
const state = useStateContext();
const dispatch = useDispatchContext();
console.log(state)
const asyncCapitalFunc = async letter => {
dispatch({type: `capital${letter}`});
};
const batchTest = async () => {
await asyncCapitalFunc('A');
await asyncCapitalFunc('B');
};
return (
<>
<p>{state.count}</p>
<button onClick={() => batchTest()}>Click</button>
</>
)
};
results:
const batchTest = async () => {
await asyncCapitalFunc('A');
await asyncCapitalFunc('B');
dispatch({type: 'capitalC'});
};
The above code: One click causes three re-renders, which is as expected. click result 1
const batchTest = async () => {
dispatch({type: 'capitalC'});
await asyncCapitalFunc('A');
await asyncCapitalFunc('B');
};
The above code: One click causes two re-renders, also as expected, the first await call is batched together with the regular dispatch above it. click result 2
const batchTest = async () => {
dispatch({type: 'capitalC'});
await asyncCapitalFunc('A');
await asyncCapitalFunc('B');
await asyncCapitalFunc('D');
await asyncCapitalFunc('E');
};
The above code: One click causes four re-renders, still as expected. click result 3
const batchTest = async () => {
await asyncCapitalFunc('A');
await asyncCapitalFunc('B');
dispatch({type: 'capitalC'});
await asyncCapitalFunc('D');
await asyncCapitalFunc('E');
};
The above code: One click causes four re-renders, still as expected. click result 4
----------------------now let’s switch the same code to Effect------------------
import { useStateContext, useDispatchContext } from "./ContextProvider"
import { useEffect } from "react";
export default function Test() {
const state = useStateContext();
const dispatch = useDispatchContext();
console.log(state)
useEffect(()=> {
const asyncCapitalFunc = async letter => {
dispatch({type: `capital${letter}`});
};
const batchTest = async () => {
await asyncCapitalFunc('A');
await asyncCapitalFunc('B');
};
batchTest()
}, [dispatch]);
return (
<>
<p>{state.count}</p>
</>
)
};
results:
const batchTest = async () => {
await asyncCapitalFunc('A');
await asyncCapitalFunc('B');
dispatch({type: 'capitalC'});
};
The above code: In addition to the initial render, there’re two re-renders, and the second await call is batched together with the dispatch below it, which is not the same as what happens in a click event. Effect result1
const batchTest = async () => {
dispatch({type: 'capitalC'});
await asyncCapitalFunc('A');
await asyncCapitalFunc('B');
};
The above code: In addition to the initial render, there’re two re-renders, this time it behaves the same as the click event.
const batchTest = async () => {
dispatch({type: 'capitalC'});
await asyncCapitalFunc('A');
await asyncCapitalFunc('B');
await asyncCapitalFunc('D');
await asyncCapitalFunc('E');
};
The above code: Two re-renders, the first await call is batched with the dispatch above it which is as expected, but why the remaining three await calls batched together? Effect result3
const batchTest = async () => {
await asyncCapitalFunc('A');
await asyncCapitalFunc('B');
dispatch({type: 'capitalC'});
await asyncCapitalFunc('D');
await asyncCapitalFunc('E');
};
The above code: I’m getting confused, why the await calls below "A" are all batched together… Effect result3
Hope someone can explain why this quirky behavior happens in Effect, thank you.