Summary

React v18’s automatic batching now groups state updates across all contexts, event handlers, timeouts, promises, and native events, into a single render. This reduces redundant renders and boosts UI responsiveness without any code changes (reactjs.org, kentcdodds.com).

1. Introduction

Excessive renders in React apps can cause jank, slow interactions, and increased CPU usage, especially when state updates fire rapidly. Pre-v18, React only batched updates inside React event handlers, leaving updates in timeouts and async callbacks unbatched, resulting in multiple renders. v18 changes that, automatically batching updates in all async contexts for smoother UIs.

2. What Changed in React v18

  • Prior Behavior: Batching in React events only; setState calls in setTimeout, promises, or native events triggered separate renders.
  • v18+ Behavior: All state updates within the same microtask are batched, regardless of origin (reactjs.org).

3. Under the Hood: React Scheduler & Flushing

React v18 introduced a new scheduler that tracks update origins and flushes a batch at the end of each tick. It uses the concurrent renderer to defer work until all sync context code finishes, then flushes updates in one go, reducing render churn and memory thrash.

4. Code Examples & Before/After

4.1 setTimeout Example

// Pre-v18
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
}, 100);
// Two renders.

// React v18
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
}, 100);
// One render. Both updates are applied together.

4.2 Promise Callback Example

// Pre-v18
fetchData().then(data => {
  setUser(data.user);
  setPosts(data.posts);
});
// Two renders.

// React v18
fetchData().then(data => {
  setUser(data.user);
  setPosts(data.posts);
});
// One render due to automatic batching.

4.3 Native Event Listener Example

// Pre-v18
document.getElementById('btn').addEventListener('click', () => {
  setClicks(c => c + 1);
  setLastClick(Date.now());
});
// Two renders.

// React v18
document.getElementById('btn').addEventListener('click', () => {
  setClicks(c => c + 1);
  setLastClick(Date.now());
});
// One render automatically.

5. Real-World Use Cases

  • Throttled Scroll Handlers: State updates in scroll events can be batched to avoid frame drops.
  • Debounced Form Inputs: Debouncing state in input handlers combined with batching reduces renders during fast typing.
  • Complex State Shapes: Multiple useState or useReducer calls in async callbacks consolidate into a single UI update.

6. Pitfalls & Best Practices

  • Forcing Immediate Renders: Use flushSync sparingly when you need to reflect state immediately (e.g., focus calls after state change) (reactjs.org).
  • Stale Closures: Batched updates still respect closure state at invocation time; ensure logic inside updates captures latest values.
  • Profiling: Use React DevTools Profiler to verify render counts before/after migration.

7. Full Example Walkthrough

Build a counter showing batched vs unbatched renders:

import { useState } from 'react';
import { flushSync } from 'react-dom';

export default function Counter() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function testBatched() {
    setTimeout(() => {
      setCount(c => c + 1);
      setFlag(f => !f);
    }, 0);
  }

  function testUnbatched() {
    setTimeout(() => {
      flushSync(() => setCount(c => c + 1));
      flushSync(() => setFlag(f => !f));
    }, 0);
  }

  console.log('Rendered');

  return (
    <div>
      <p>Count: {count}</p>
      <p>Flag: {flag.toString()}</p>
      <button onClick={testBatched}>Batched Timeout</button>
      <button onClick={testUnbatched}>Unbatched (flushSync)</button>
    </div>
  );
}
  • Clicking “Batched Timeout” causes a single render (one “Rendered” log).
  • “Unbatched” forces two renders via flushSync.

8. Conclusion & Further Reading

Automatic batching in React v18 makes your apps smoother without code changes. It consolidates async updates into single renders, cutting unnecessary work. When you do need immediate updates, flushSync is available. For more details, read the React 18 release blog and scheduler architecture docs.

Further Reading