React Automatic Batching
Understanding how React groups state updates for better performance
⚡ What is Automatic Batching?
Automatic batching is React's way of grouping multiple state updates into a single re-render for better performance. React 18 automatically batches updates everywhere, including promises, timeouts, and event handlers.
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setCount(c => c + 1); // Doesn't re-render yet
setFlag(f => !f); // Batched together - one re-render
}
}
Result:
Both state updates trigger only ONE re-render instead of two.
Batching Concepts
Event Handlers
Batches updates in click handlers
onClick={() => {
setState1(x);
setState2(y);
// One render
}}
Async Functions
Batches in promises & timeouts
setTimeout(() => {
setState1(x);
setState2(y);
// One render (React 18+)
}, 1000);
Native Events
Batches in native event listeners
element.addEventListener('click', () => {
setState1(x);
setState2(y);
// One render
});
Performance
Reduces unnecessary re-renders
// Multiple updates
// = Single render
// = Better performance
🔹 Batching in Event Handlers
React automatically batches state updates inside event handlers. Multiple setState calls result in only one re-render, improving performance significantly.
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
console.log('Component rendered');
function handleClick() {
// Both updates are batched together
setCount(count + 1);
setFlag(!flag);
// Only ONE re-render happens here
}
return (
<div>
<p>Count: {count}</p>
<p>Flag: {flag.toString()}</p>
<button onClick={handleClick}>Update Both</button>
</div>
);
}
Output:
Console shows "Component rendered" only ONCE per click, not twice.
🔹 Batching in Async Code (React 18+)
React 18 introduced automatic batching everywhere, including promises, setTimeout, and native event handlers. Previously, these required manual batching with ReactDOM.unstable_batchedUpdates().
import React, { useState } from 'react';
function AsyncBatching() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
console.log('Rendered');
function handleClick() {
// Fetch data from API
fetch('/api/data').then(() => {
// React 18: These are batched automatically
setCount(c => c + 1);
setFlag(f => !f);
// Only ONE re-render
});
// Also works with setTimeout
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// Only ONE re-render
}, 1000);
}
return (
<div>
<p>Count: {count}</p>
<p>Flag: {flag.toString()}</p>
<button onClick={handleClick}>Async Update</button>
</div>
);
}
Output:
In React 18+, async updates are batched. In React 17, they would cause separate re-renders.
🔹 Opting Out of Batching
Sometimes you need immediate updates without batching. Use flushSync() to force synchronous updates, though this should be rare as it reduces performance.
import React, { useState } from 'react';
import { flushSync } from 'react-dom';
function OptOutBatching() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
// Force immediate update (not batched)
flushSync(() => {
setCount(c => c + 1);
});
// Component re-renders here
// Another immediate update
flushSync(() => {
setFlag(f => !f);
});
// Component re-renders again
// Total: TWO re-renders instead of one
}
return (
<div>
<p>Count: {count}</p>
<p>Flag: {flag.toString()}</p>
<button onClick={handleClick}>Force Sync Updates</button>
</div>
);
}
Output:
⚠️ Use flushSync() sparingly - it bypasses batching and reduces performance.
🔹 React 17 vs React 18 Batching
Understanding the difference between React versions helps when upgrading or debugging batching behavior in your applications.
React 17 (Old Behavior):
- ✅ Batched in React event handlers
- ❌ NOT batched in promises
- ❌ NOT batched in setTimeout
- ❌ NOT batched in native event listeners
React 18 (New Behavior):
- ✅ Batched in React event handlers
- ✅ Batched in promises
- ✅ Batched in setTimeout
- ✅ Batched in native event listeners
- ✅ Batched everywhere automatically!
// React 17: Two re-renders
setTimeout(() => {
setCount(c => c + 1); // Re-render 1
setFlag(f => !f); // Re-render 2
}, 1000);
// React 18: One re-render
setTimeout(() => {
setCount(c => c + 1); // Batched
setFlag(f => !f); // Batched
}, 1000); // Only one re-render!
🔹 Best Practices
Follow these guidelines to make the most of automatic batching:
- Upgrade to React 18+ for automatic batching everywhere
- Use functional updates when new state depends on old state
- Avoid flushSync() unless absolutely necessary
- Trust automatic batching - React handles it for you
- Don't manually optimize what React already optimizes
// ✅ Good: Functional updates
setCount(c => c + 1);
setFlag(f => !f);
// ❌ Avoid: Direct state reference (can be stale)
setCount(count + 1);
setFlag(!flag);