Automatic Batching: Inconsistent Render Counts Across Event and Timer Callbacks
0 reputation · 15 May 2021, 16:32 UTC
0 reputation · 15 May 2021, 16:32 UTC
Determine the precise conditions under which React 18's automatic batching groups multiple state updates into a single render.
Automatic batching is documented to apply to event handlers and promise callbacks but excludes timer-based callbacks outside React's control. In Concurrent Mode, the scheduler may further delay batched updates until the next microtask. The behavior also differs between React DOM and React Native, and the opt-out API unstable_batchedUpdates is deprecated in React 19.
35775 reputation · 15 May 2021, 18:12 UTC
With a concurrent root created via createRoot, automatic batching applies to updates issued within a single synchronous callback segment. This includes:
setTimeout/setInterval callbacks.then/.catch/.finally handlersaddEventListener when they run in the same taskLegacy roots created with ReactDOM.render retain the older behavior where only React synthetic event handlers batch updates.
Multiple setState calls inside the same callback segment (e.g., one setTimeout handler or one .then block) typically produce one committed render. However:
setTimeout calls) or across await boundaries are different scheduling turns and are not guaranteed to batch.flushSync forces a synchronous commit and breaks batching.setCount(c => c + 1)) or a single state object/useReducer to express multiple logical changes as one update.setTimeout calls or await points when you need them batched.unstable_batchedUpdates (deprecated/unavailable in React 19-era releases).// React DOM with createRoot
import { createRoot } from 'react-dom/client';
import { useState, useEffect } from 'react';
function Counter() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
useEffect(() => { console.log('commit', a, b); });
return null;
}
const root = createRoot(document.getElementById('root'));
root.render(<Counter />);
// Same callback -> expect 1 commit
setTimeout(() => { setA(1); setB(2); }, 0);
// Separate timers -> may produce 2 commits
setTimeout(() => setA(3), 0);
setTimeout(() => setB(4), 0);
// Before/after await -> different scheduling turns
Promise.resolve().then(async () => {
setA(5);
await Promise.resolve();
setB(6);
});
Repeat the same component under React Native and log both JS commits (useEffect) and native view updates. Treat batching as consistent only if both platforms group updates from the same callback segment identically.
What exact React version (including minor/patch) and renderer package (react-dom vs react-native) are you targeting? The stable batching API that replaces unstable_batchedUpdates is version-sensitive.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.