Back to Blog
By AriesZhou · · 6 min read

React Batching Updates Notes

React

Batching is a core optimization technique in React that combines multiple state updates into a single render to improve performance. Understanding the batching mechanism is key to writing more efficient React.

1. Batching

Batching is a mechanism that combines multiple state updates into a single render. Consider the following scenario:

function Counter() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  function handleClick() {
    setCount1(count1 + 1);
    setCount2(count2 + 1);
    // 两次状态更新,React 会合并为一次渲染
  }

  return (
    <div>
      <p>Count 1: {count1}</p>
      <p>Count 2: {count2}</p>
      <button onClick={handleClick}>增加</button>
    </div>
  );
}

After clicking the button, although setState is called twice, the component re-renders only once. This is the effect of batching.

2. Batching in React 17 and Earlier

2.1 Implementation Principle

// React 17 简化版实现
let isBatchingUpdates = false;
let updateQueue = [];

function batchedUpdates(callback) {
  const alreadyBatchingUpdates = isBatchingUpdates;
  isBatchingUpdates = true;

  try {
    return callback();
  } finally {
    isBatchingUpdates = alreadyBatchingUpdates;
    if (!isBatchingUpdates) {
      flushBatchedUpdates();
    }
  }
}

function flushBatchedUpdates() {
  while (updateQueue.length) {
    const update = updateQueue.shift();
    update.perform();
  }
}

Key points:

  • isBatchingUpdates flag determines whether batching is in progress
  • batchedUpdates function enables batching mode, flushes the queue after executing the callback
  • React automatically enables batching in event handlers

2.2 Limitations

In React 17 and earlier, batching is mainly limited to event handlers:

// React 17: setTimeout 中的更新不会批量处理
function handleClick() {
  setTimeout(() => {
    setCount1(c => c + 1); // 触发一次渲染
    setCount2(c => c + 1); // 再触发一次渲染
  }, 1000);
}

Note: state updates in async callbacks are not batched; each setState triggers a separate render.


3. Automatic Batching in React 18

3.1 Core Improvements

React 18 introduces automatic batching, which merges state updates even inside asynchronous code:

import React, { useState } from 'react';
import ReactDOM from 'react-dom/client';

function Counter() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  function handleAsyncUpdate() {
    setTimeout(() => {
      setCount1(c => c + 1); // 自动合并
      setCount2(c => c + 1); // 自动合并
      // 只会触发一次渲染!
    }, 1000);
  }

  return (
    <div>
      <p>Count 1: {count1}</p>
      <p>Count 2: {count2}</p>
      <button onClick={handleAsyncUpdate}>异步更新</button>
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Counter />);

3.2 Performance Gains

ScenarioReact 17React 18
Multiple updates in event handlers1 render1 render
Multiple updates in setTimeoutMultiple renders1 render
Multiple updates in Promise.thenMultiple renders1 render
Multiple updates in fetch callbacksMultiple renders1 render

3.3 Manual Batching

If you need to batch updates manually in specific scenarios, you can use unstable_batchedUpdates:

import { unstable_batchedUpdates } from 'react-dom';

function handleClick() {
  unstable_batchedUpdates(() => {
    setCount1(c => c + 1);
    setCount2(c => c + 1);
  });
}

Tip: In React 18, most scenarios don’t require manual calls; the framework handles it automatically.

4. Batching flow diagram

graph TD
    A[状态更新触发] --> B{是否在批量上下文中?}
    B -->|是| C[加入更新队列]
    B -->|否| D[立即处理更新]
    C --> E{批量操作结束?}
    E -->|否| F[等待更多更新]
    E -->|是| G[执行批量渲染]
    D --> H[执行单次渲染]
    G --> I[组件重新渲染]
    H --> I

5. Combining with other React 18 features

5.1 Transition API

Batching combined with startTransition lets you distinguish urgent updates from non-urgent ones:

import { useTransition, useState } from 'react';

function SearchResults() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  function handleChange(e) {
    const value = e.target.value;
    setQuery(value); // 紧急更新 - 输入框立即响应

    startTransition(() => {
      // 非紧急更新 - 搜索结果批量处理
      searchAPI(value);
    });
  }
}

5.2 Concurrent rendering

React 18’s concurrent rendering allows interrupting and resuming renders, marking a major architectural upgrade for React.

5.2.1 What is concurrent rendering

Traditional React (17 and earlier) rendering is synchronous: once rendering starts, it blocks the main thread until it completes. If the component tree is large, user interactions may be delayed, causing jank.

Concurrent rendering is different: React can prepare multiple versions of the UI simultaneously and switch between them dynamically based on the priority of user interactions.

graph TD
    A[用户输入] --> B{紧急更新?}
    B -->|是| C[高优先级渲染]
    B -->|否| D[低优先级渲染]
    C --> E[立即响应]
    D --> F[可中断]
    F --> G[更紧急的更新到达]
    G --> C
    F --> H[完成渲染]

5.2.2 Core concept: Fiber architecture

React 18’s concurrent rendering is built on the Fiber architecture. Fiber is an internal React data structure that splits rendering work into small units:

// 传统渲染:一次性完成
// 组件A → 组件B → 组件C → DOM 更新

// Fiber 渲染:分片完成
// [工作单元1] → [工作单元2] → [工作单元3] → ... → DOM 更新
//     ↑              ↓
//   中断          恢复

5.2.3 useDeferredValue

useDeferredValue is used to defer rendering of non-critical content:

import { useDeferredValue, useState, useMemo } from 'react';

function SearchResults() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);

  // 大量数据的筛选 - 使用延迟值
  const filteredItems = useMemo(
    () => items.filter(item =>
      item.name.toLowerCase().includes(deferredQuery.toLowerCase())
    ),
    [deferredQuery]
  );

  // 样式区分:延迟渲染时显示不同状态
  const isStale = query !== deferredQuery;

  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="搜索..."
      />
      <div style={{ opacity: isStale ? 0.5 : 1 }}>
        {filteredItems.map(item => (
          <div key={item.id}>{item.name}</div>
        ))}
      </div>
    </div>
  );
}

5.2.4 useTransition

startTransition marks non-urgent updates:

import { useTransition, useState } from 'react';

function TabContainer() {
  const [isPending, startTransition] = useTransition();
  const [activeTab, setActiveTab] = useState('posts');

  function handleTabChange(tab) {
    // 立即更新 UI 状态
    setActiveTab(tab);

    // 但内容切换可以延迟
    startTransition(() => {
      // 这个更新会被标记为低优先级
      fetchTabData(tab);
    });
  }

  return (
    <div>
      <button onClick={() => handleTabChange('posts')}>文章</button>
      <button onClick={() => handleTabChange('comments')}>评论</button>
      <button onClick={() => handleTabChange('settings')}>设置</button>

      {isPending && <LoadingSpinner />}
      <TabContent activeTab={activeTab} />
    </div>
  );
}

5.2.5 Concurrent rendering workflow

sequenceDiagram
    participant User as 用户
    participant React as React 引擎
    participant Fiber as Fiber 调度器
    participant DOM as DOM

    User->>React: 输入搜索关键词
    React->>Fiber: 创建高优先级任务
    Fiber->>Fiber: 中断低优先级渲染
    Fiber->>DOM: 优先更新输入框
    Note over Fiber: 用户看到即时反馈
    Fiber->>DOM: 完成搜索结果渲染
    Note over DOM: 显示搜索结果

5.2.6 Use case comparison

ScenarioSolutionNotes
Search input + results listuseDeferredValueInput responds immediately, results render deferred
Tab switchinguseTransitionSwitch button responds immediately, content can be deferred
Large list scrollinguseDeferredValueKeeps scrolling smooth
Form validationNo handling neededValidation is typically urgent

5.2.7 Caveats

// ❌ 错误:不要对状态值使用 useDeferredValue
function BadExample() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);

  // 这没有意义,query 和 deferredQuery 本质相同
  return <div>{deferredQuery}</div>;
}

// ✅ 正确:deferredQuery 用于派生计算
function GoodExample() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);

  const filteredItems = useMemo(
    () => expensiveFilter(allItems, deferredQuery),
    [deferredQuery]
  );

  return <List items={filteredItems} />;
}

5.2.8 Relationship with batching updates

Concurrent rendering and batching updates are complementary:

function App() {
  const [count, setCount] = useState(0);
  const [isPending, startTransition] = useTransition();

  function handleClick() {
    setCount(c => c + 1); // 高优先级更新 - 自动批量

    startTransition(() => {
      // 低优先级更新 - 可中断
      setFilterValue(newValue);
      setSearchResults(newResults);
    });
  }
}
  • Batching updates: merges multiple setState into a single render
  • Concurrent rendering: Determines render order and whether to interrupt based on priority

Combined, React 18 can intelligently handle various update scenarios, providing a smooth user experience.

6. Summary

React’s batching update mechanism has undergone significant evolution:

  • React 17: Automatic batching only in event handlers
  • React 18: Automatic batching extended to all scenarios

This improvement brings notable performance gains. Developers no longer need to worry about the context of state updates; React intelligently merges renders.