React 18 comes with automatic batching support for state updates. This helps in avoiding multiple renders for state updates in promises, setTimeout, setInterval, native event handlers in addition to react event handlers. Thus, we get performance improvement in our React apps out of the box because of automatic batching.
What is (automatic) batching?
Batching is the process of grouping multiple state updates into a single update. If we have multiple calls to set state of a component, React combines those updates in a single update call (called as a batch), resulting in a single re-render of the component. As React figures this out automatically and updates the state in a batch, it is called as Automatic Batching.
Let's see how it works by considering various ways of updating the state.
1. State updates for event handlers in React 17 & before
Let's take an example to understand how rendering happens upon state updates in React 17 on event handlers.
const Counter = () => {
const [count, setCount] = useState(0);
const [showModal, setShowModal] = useState(false);
const handleClick = () => {
setCount(count + 1);
setShowModal((prev) => !prev);
// React renders once at the end (that's batching)
};
console.log('rendered component');
return (
<div>
<p>You clicked {count} times</p>
<p>{`Show modal? ${showModal}`}</p>
<button onClick={handleClick}>Click me</button>
</div>
);
}
Now, as we can see there are two calls for the state update. One for setCount and another for setShowModal. But, react has made sure that only one render is called at the end.
If the updates are not batched, it would render the component in half baked manner, resulting in UI flickering. i.e. We want our component to be rendered only after count is updated and showModal flag is updated.
Since, react has control over the event handlers, this is available in React since beginning.
2. State updates for promises, native event handlers in React 17 & before
Automatic Batching does not work for state updates in promises / non react handlers like setTimeout, setInterval etc.
Let's take an example.
const Counter = () => {
const [count, setCount] = useState(0);
const [showModal, setShowModal] = useState(false);
const handleClick = () => {
console.log('fetch called');
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(() => {
setCount(count + 1); // Re-render is called
setShowModal((prev) => !prev); // Re-render is called
})
};
console.log('rendered component');
return (
<div>
<p>You clicked {count} times</p>
<p>{`Show modal? ${showModal}`}</p>
<button onClick={handleClick}>Click me now</button>
</div>
);
}
Usually, we call an API request to fetch something and perform state updates in the callback based on the response from the API request. As we can see in the above example, there are 2 calls to set the state in the callback resulting in 2 re-renders.
This is a performance bottleneck. This can result in UI flickering, rendering the result of a partial state update.
3. State updates for promises, native event handlers in React 18+
React has fixed this issue by providing automatic batching support for state updates in promises, setTimeout and setInterval, native event handlers in addition to default react event handlers.
Note: We have updated react and react-dom library versions as given below, for this example.
https://unpkg.com/react@18.0.0-beta-24dd07bd2-20211208/umd/react.development.js
https://unpkg.com/react-dom@18.0.0-beta-24dd07bd2-20211208/umd/react-dom.development.js
If we take the same example as given above, we can see number of renders below.
const Counter = () => {
const [count, setCount] = useState(0);
const [showModal, setShowModal] = useState(false);
const handleClick = () => {
console.log('fetch called');
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(() => {
setCount(count + 1);
setShowModal((prev) => !prev);
// React 18 renders once at the end (that's automatic batching)
})
};
console.log('rendered component');
return (
<div>
<p>You clicked {count} times</p>
<p>{`Show modal? ${showModal}`}</p>
<button onClick={handleClick}>Click me</button>
</div>
);
}
Note: we need to upgrade render
to createRoot.
For the example given above, we have rendered Counter
component as given below.
We have created root using ReactDOM.createRoot
and then rendered Counter
component on the same.
ReactDOM.createRoot(document.getElementById('app')).render(<Counter /> );
Please follow official discussion on createRoot and automatic batching on the reactwg/react-18 repository for more details.