Как работает automatic batching в React 18?

SeniorReact · Frontend·Обновлено 6 августа 2026
Коротко
Automatic batching в React 18 — это автоматическое объединение нескольких вызовов setState в один рендер, которое теперь работает везде: в setTimeout, Promise, нативных обработчиках и кастомных событиях, а не только внутри React-событий как раньше.

Что такое automatic batching

Batching — это механизм объединения нескольких обновлений состояния в один цикл рендера. Вместо того чтобы рендерить компонент после каждого вызова setState, React собирает все изменения и применяет их за один проход.

Как было до React 18

В React 17 и ранее batching работал только внутри синтетических React-событий (например, onClick). За пределами React-контекста — в setTimeout, Promise.then, fetch-колбэках, нативных addEventListener — каждый setState вызывал отдельный рендер.

// React 17: два отдельных рендера
setTimeout(() => {
  setCount(c => c + 1); // рендер №1
  setFlag(f => !f);     // рендер №2
}, 1000);

Как работает в React 18

С введением нового root API (createRoot) React 18 автоматически batches все обновления состояния независимо от контекста их вызова.

import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root')!);
root.render(<App />);

Теперь тот же код с setTimeout даст только один рендер:

// React 18 с createRoot: один рендер
setTimeout(() => {
  setCount(c => c + 1); // \u043eбъединяется
  setFlag(f => !f);     // объединяется
  // рендер происходит один раз после обоих вызовов
}, 1000);

// Promise-цепочка: тоже один рендер
fetch('/api/data').then(() => {
  setData(newData);
  setLoading(false);
});

Механизм под капотом

Automatic batching опирается на новый планировщик (scheduler) и механизм transitions. React 18 оборачивает обновления в единую «задачу» внутри event loop, накапливая все setState-вызовы до завершения текущего синхронного блока кода, после чего запускает один reconciliation.

Как отказаться от batching

Если нужно принудительно применить обновление немедленно — используйте flushSync из react-dom:

import { flushSync } from 'react-dom';

flushSync(() => {
  setCount(c => c + 1); // немедленный рендер
});
// DOM уже обновлён здесь
flushSync(() => {
  setFlag(f => !f); // ещё один немедленный рендер
});

Важный нюанс совместимости

Automatic batching активируется только при использовании createRoot. Если приложение использует устаревший ReactDOM.render, оно работает в legacy-режиме без расширенного batching — поведение остаётся как в React 17.

Практический эффект

Автоматический batching снижает количество лишних рендеров, особенно заметно в компонентах, которые обновляют несколько состояний в async-контекстах: обработчики websocket-сообщений, fetch-ответы, debounced-функции. Это ускоряет UI без каких-либо изменений в коде приложения.

Что хочет услышать интервьюер

Понимание разницы в поведении batching между React 17 и React 18 — особенно вне React-событий

Знание того, что batching в React 18 требует подключения через createRoot, а не legacy ReactDOM.render

Умение объяснить, как flushSync позволяет принудительно выйти из batching когда это необходимо

Понимание практической пользы: меньше рендеров в async-контекстах (setTimeout, Promise, fetch)

Осведомлённость о связи automatic batching с новым планировщиком и concurrent-режимом React 18

Пример: Сравнение поведения React 17 и React 18

// React 17: два рендера в setTimeout
setTimeout(() => {
  setCount(c => c + 1); // рендер №1
  setFlag(f => !f);     // рендер №2
}, 500);

// React 18 с createRoot: один рендер
setTimeout(() => {
  setCount(c => c + 1); // объединяется
  setFlag(f => !f);     // объединяется
  // один рендер после оба вызова
}, 500);

Пример: createRoot и отключение batching через flushSync

import { createRoot } from 'react-dom/client';
import { flushSync } from 'react-dom';

// Подключение React 18 с automatic batching
const root = createRoot(document.getElementById('root')!);
root.render(<App />);

// Принудительный немедленный рендер через flushSync
function handleClick() {
  flushSync(() => {
    setCount(c => c + 1); // немедленный рендер
  });
  // DOM уже обновлён, можно читать layout
  flushSync(() => {
    setFlag(f => !f); // ещё один немедленный рендер
  });
}

Пример: Практический пример с WebSocket

// Реальный кейс: обработчик WebSocket
useEffect(() => {
  const ws = new WebSocket('wss://example.com');

  ws.onmessage = (event) => {
    const data = JSON.parse(event.data);
    // React 18: оба setState объединяются в один рендер
    // React 17: было бы два отдельных рендера
    setMessages(prev => [...prev, data.message]);
    setLastUpdated(Date.now());
  };

  return () => ws.close();
}, []);

Типичные ошибки

Считают, что batching в React 17 работал везде — не знают об ограничении только React-событиями

Путают automatic batching с Concurrent Mode — это разные, хотя и связанные фичи React 18

Не знают про flushSync и не могут объяснить, как отключить batching при необходимости

Думают, что automatic batching появляется автоматически без замены ReactDOM.render на createRoot

Не могут привести конкретный пример, где batching реально уменьшает количество рендеров

Лучшие курсы по теме

изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Feature-Sliced Design

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее