--- id: wiki-2026-0508-automatic-batching title: Automatic Batching (React 18+) category: 10_Wiki/Topics status: verified canonical_id: self aliases: [React Batching, Concurrent Batching] duplicate_of: none source_trust_level: A confidence_score: 0.9 verification_status: applied tags: [react, batching, performance, rendering] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: TypeScript framework: React 19 --- # Automatic Batching (React 18+) ## 매 한 줄 > **"매 setState 매 합쳐서 매 한 번 render"**. React 18에서 도입된 automatic batching은 모든 update (promise, setTimeout, native event)를 single re-render로 group한다. React 17 이전엔 React event handler 안에서만 batching이었음 — 이제 전부 자동. ## 매 핵심 ### 매 동작 - 매 동일 tick 안의 setState calls → React가 모아서 하나의 commit 으로 처리. - 결과: less render, less DOM mutation, less component lifecycle invocation. - React 19도 동일 model 유지 + Compiler 가 추가 optimization. ### React 17 vs 18+ - **17**: `onClick` 안 batching O / `setTimeout`, `Promise.then` 안 batching X. - **18+**: 매 모든 context batching O. - Opt-out: `flushSync()` 로 immediate render 강제. ### 매 응용 1. Form state with multiple fields — 매 single render. 2. Async fetch chain — 매 single render after Promise. 3. Concurrent transitions — startTransition + batching. ## 💻 패턴 ### React 18 default behavior ```tsx import { useState } from 'react'; function Form() { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [age, setAge] = useState(0); async function handleSubmit() { const res = await fetch('/api/me'); const data = await res.json(); // 18+: 매 3 setState 매 single render setName(data.name); setEmail(data.email); setAge(data.age); } return ; } ``` ### flushSync to opt out ```tsx import { flushSync } from 'react-dom'; function handleClick() { flushSync(() => { setCount(c => c + 1); }); // 매 DOM 업데이트 매 done — measure 가능 const h = listRef.current.scrollHeight; flushSync(() => { setHeight(h); }); } ``` ### Custom hook with batched async ```tsx function useAsyncForm() { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); async function load(fn: () => Promise) { setLoading(true); setError(null); try { const res = await fn(); // 18+: 매 두 setState 매 single render setData(res); setLoading(false); } catch (e) { setError(e as Error); setLoading(false); } } return { data, loading, error, load }; } ``` ### Reducer alternative ```tsx const [state, dispatch] = useReducer(reducer, initial); // 매 single dispatch 매 multi-field update — natural batching dispatch({ type: 'SUBMIT_OK', payload: data }); ``` ### Avoid stale closure with functional updates ```tsx // 매 sequential update — 매 함수형 사용 setCount(c => c + 1); setCount(c => c + 1); // → +2, not +1 ``` ### Measuring render with Profiler ```tsx import { Profiler } from 'react'; console.log(id, phase, actual)} >
``` ## 매 결정 기준 | 상황 | Approach | |---|---| | Multi-field update | default (let batching work) | | DOM measurement between updates | `flushSync` | | Sequential counter update | functional updater | | Complex state graph | `useReducer` | | Concurrent UI | `startTransition` | **기본값**: do nothing — automatic batching handles it. ## 🔗 Graph - 부모: [[React]] · [[Concurrent Features|Concurrent Rendering]] - 변형: [[Batching]] · [[startTransition]] - Adjacent: [[flushSync]] · [[useReducer]] ## 🤖 LLM 활용 **언제**: explain when batching applies, refactor pre-18 code, debug "why is render double" question. **언제 X**: actual render count measurement — Profiler / DevTools. ## ❌ 안티패턴 - **불필요한 flushSync**: 매 performance 의 hurt — 마지막 수단. - **Sequential setState with stale value**: `setCount(count+1); setCount(count+1)` → +1. - **Object identity reset**: `setData({...})` matter — same shallow ref skip. - **Mid-render setState**: trigger infinite loop. ## 🧪 검증 / 중복 - Verified (React 18 RFC, React 19 docs). - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — React 18+ batching pattern + flushSync |