Reducing Render Noise with React 18 Automatic Batching
Stop unnecessary re-renders in async contexts. Learn how React 18's automatic batching groups state updates in promises and timeouts into a single render cycle.
06 Nov 2025, 07:03 UTC

The Problem: The "Double Render" Performance Hit
In older versions of React, state updates were only batched (grouped together) when they occurred inside React event handlers. If you updated two different pieces of state inside a setTimeout, a fetch call, or a native browser event listener, React would trigger a separate render for every single state update. This led to "render noise"—unnecessary UI recalculations that could cause lag or flickering in complex components.
The takeaway is simple: React 18 introduces Automatic Batching. Now, regardless of where the state updates originate, React groups them into a single render cycle, significantly reducing the workload on the browser.
How Automatic Batching Works
Batching is the process of grouping multiple state updates into one single re-render. In React 17 and below, this was limited to React's own synthetic event system. If you used an async function, the batching "broke," and each setState call forced the component to re-render immediately.
React 18 changes this by utilizing the JavaScript event loop more effectively. When multiple state updates happen within the same "tick" (the smallest unit of execution in the JS engine), React waits until the end of that execution block to apply the changes to the DOM. This ensures that the UI only updates once, even if you've modified five different state variables in a single promise resolution.
Practical Example: Async State Updates
Consider a scenario where a user clicks a button to load a profile. You need to update both the isLoading status and the userData state after a network request completes.
import React, { useState } from 'react';
function UserProfile() {
const [isLoading, setIsLoading] = useState(false);
const [userData, setUserData] = useState(null);
const handleFetchUser = () => {
setIsLoading(true);
// Simulating an API call
setTimeout(() => {
// In React 17, these two calls would cause TWO renders
// In React 18, these are batched into ONE render
setUserData({ name: 'Jane Doe' });
setIsLoading(false);
}, 1000);
};
return (
<div>
{isLoading ? <p>Loading...</p> : <p>User: {userData?.name}</p>}
<button onClick={handleFetchUser}>Load Profile</button>
</div>
);
}
Verification Steps
To confirm this is working in your project, follow these steps:
- Ensure you are using React 18+ and have migrated from
ReactDOM.renderto theReactDOM.createRootAPI. - Open the React DevTools Profiler in your browser.
- Start a recording and trigger the async function above.
- Check the "Commits" section. You should see only one commit for the two state updates, rather than two separate commits.
When Batching is a Limitation
While automatic batching is generally a win, it creates a challenge if you need to perform a DOM operation immediately after a state change. Because React waits until the end of the function to render, the DOM will not reflect the state change until the function completes.
If you have a rare case where you must force an immediate update—such as calculating the scroll position of a newly rendered element—you can use flushSync from react-dom.
import { flushSync } from 'react-dom';
function ScrollComponent() {
const [items, setItems] = useState([]);
const addItem = () => {
// flushSync forces React to update the DOM immediately
flushSync(() => {
setItems([...items, 'New Item']);
});
// Now the DOM is updated, and we can safely measure it
document.getElementById('list').scrollTop = 500;
};
}
Risk: Use flushSync sparingly. Overusing it defeats the purpose of batching and can degrade performance by forcing the browser to perform multiple expensive layout calculations.
Summary Checklist
- Requirement: React 18+ and
createRootAPI. - Behavior: Promises,
setTimeout, and native events now batch state updates by default. - Check: Use React DevTools Profiler to verify single commits.
- Escape Hatch: Use
flushSynconly when immediate DOM access is required.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.