React 18 automatic batching patterns
Classify React 18 batching regressions, choose refactor versus flushSync, and update tests so class components no longer depend on React 17 intermediate renders in class-component async/await flows.
When to invoke
- "Fix this React 18 batching regression in a class component."
- "This async method calls setState twice and reads this.state after await."
- "Should I use flushSync here?"
- "Our React 18 tests fail because intermediate state no longer renders."
- "Multiple setState calls in setTimeout or Promise .then() behave differently after upgrade."
Batching change matrix
Location of setState |
React 17 behavior |
React 18 behavior |
Review action |
| React event handler |
Batched |
Batched |
Usually no change. |
setTimeout |
Immediate re-render |
Batched |
Check for code or tests that expect intermediate state. |
Promise .then() / Promise .catch() |
Immediate re-render |
Batched |
Refactor state reads and assertions. |
async / await continuation |
Immediate re-render |
Batched |
Treat this.state reads after await as high-risk. |
Native addEventListener callback |
Immediate re-render |
Batched |
Use React state rules even outside React synthetic events. |
Batched means all setState calls in that execution context flush together in one render at the end; no intermediate render is visible.
Diagnosis categories
| Category |
Signal |
Default fix |
Read next |
| A: silent state-read bug |
Code reads this.state after await, setTimeout, or a Promise callback and uses it for a decision. |
Compute the next value locally, use functional setState, or move dependent work into the callback. |
references/batching-categories.md |
B: refactor, no flushSync |
Intermediate render is not user-visible; only code structure or tests assume React 17 timing. |
Collapse updates, assert final state, and remove timing assumptions. |
references/batching-categories.md |
C: flushSync justified |
User must see a loading/spinner/progress state, including a spinner/loading transition, before expensive sync work or an async operation begins. |
Wrap only the minimum state update in flushSync. |
references/flushSync-guide.md |
flushSync decision rule
Use flushSync sparingly. It forces a synchronous render and bypasses React 18's concurrent scheduler, so overuse removes the performance benefit of automatic batching.
Use flushSync only when |
Do not use flushSync when |
| A spinner or loading state must render before a fetch or expensive operation starts. |
The next line merely needs a value; store it in a local variable instead. |
| Sequential UI steps must be visibly distinct, such as a progress wizard or multi-step flow. |
A test asserts an intermediate render that users cannot observe. |
| Browser measurement must happen after a specific state update reaches the DOM. |
Multiple state updates can be represented as one final state. |
Progressive disclosure and bundled resources
references/batching-categories.md: Category A, B, and C explanations with full before/after code.
references/flushSync-guide.md: flushSync import syntax, permitted use cases, and anti-patterns.
Read these references only when the active file matches the category or the user is about to add flushSync.
Output template
## React 18 batching result - <component or file>
**Status:** fixed | recommendation only | blocked
**Category:** A state-read bug | B refactor/no flushSync | C flushSync justified
| Evidence | Decision | Change |
| --- | --- | --- |
| `<setState location and state read>` | `<why React 18 batching matters>` | `<refactor, test update, or flushSync>` |
### Validation
- Tests run: `<command or not run>`
- Intermediate state dependency removed or justified: `<yes/no>`
- `flushSync` used: `<no or minimal location and reason>`
Quality gate
1---2name: react18-batching-patterns3description: Diagnose and fix React 18 automatic batching regressions in class components. Use when multiple setState calls occur after await, inside setTimeout, Promise .then() or .catch(), native addEventListener callbacks, or when tests fail because they assert intermediate state after a React 18 upgrade.4---56<!-- Generated from harness/github-copilot/skills/react18-batching-patterns/SKILL.md by harness/claude-code/scripts/convert_from_copilot.py. Edit the source, not this file. -->78# React 18 automatic batching patterns910Classify React 18 batching regressions, choose refactor versus `flushSync`, and update tests so class components no longer depend on React 17 intermediate renders in class-component `async/await` flows.1112## When to invoke1314- "Fix this React 18 batching regression in a class component."15- "This async method calls setState twice and reads this.state after await."16- "Should I use flushSync here?"17- "Our React 18 tests fail because intermediate state no longer renders."18- "Multiple setState calls in setTimeout or Promise .then() behave differently after upgrade."1920## Batching change matrix2122| Location of `setState` | React 17 behavior | React 18 behavior | Review action |23| --- | --- | --- | --- |24| React event handler | Batched | Batched | Usually no change. |25| `setTimeout` | Immediate re-render | Batched | Check for code or tests that expect intermediate state. |26| `Promise .then()` / `Promise .catch()` | Immediate re-render | Batched | Refactor state reads and assertions. |27| `async` / `await` continuation | Immediate re-render | Batched | Treat `this.state` reads after `await` as high-risk. |28| Native `addEventListener` callback | Immediate re-render | Batched | Use React state rules even outside React synthetic events. |2930`Batched` means all `setState` calls in that execution context flush together in one render at the end; no intermediate render is visible.3132## Diagnosis categories3334| Category | Signal | Default fix | Read next |35| --- | --- | --- | --- |36| A: silent state-read bug | Code reads `this.state` after `await`, `setTimeout`, or a Promise callback and uses it for a decision. | Compute the next value locally, use functional `setState`, or move dependent work into the callback. | `references/batching-categories.md` |37| B: refactor, no `flushSync` | Intermediate render is not user-visible; only code structure or tests assume React 17 timing. | Collapse updates, assert final state, and remove timing assumptions. | `references/batching-categories.md` |38| C: `flushSync` justified | User must see a loading/spinner/progress state, including a spinner/loading transition, before expensive sync work or an async operation begins. | Wrap only the minimum state update in `flushSync`. | `references/flushSync-guide.md` |3940## flushSync decision rule4142Use `flushSync` sparingly. It forces a synchronous render and bypasses React 18's concurrent scheduler, so overuse removes the performance benefit of automatic batching.4344| Use `flushSync` only when | Do not use `flushSync` when |45| --- | --- |46| A spinner or loading state must render before a fetch or expensive operation starts. | The next line merely needs a value; store it in a local variable instead. |47| Sequential UI steps must be visibly distinct, such as a progress wizard or multi-step flow. | A test asserts an intermediate render that users cannot observe. |48| Browser measurement must happen after a specific state update reaches the DOM. | Multiple state updates can be represented as one final state. |4950## Progressive disclosure and bundled resources5152- `references/batching-categories.md`: Category A, B, and C explanations with full before/after code.53- `references/flushSync-guide.md`: `flushSync` import syntax, permitted use cases, and anti-patterns.5455Read these references only when the active file matches the category or the user is about to add `flushSync`.5657## Output template5859```markdown60## React 18 batching result - <component or file>6162**Status:** fixed | recommendation only | blocked63**Category:** A state-read bug | B refactor/no flushSync | C flushSync justified6465| Evidence | Decision | Change |66| --- | --- | --- |67| `<setState location and state read>` | `<why React 18 batching matters>` | `<refactor, test update, or flushSync>` |6869### Validation70- Tests run: `<command or not run>`71- Intermediate state dependency removed or justified: `<yes/no>`72- `flushSync` used: `<no or minimal location and reason>`73```7475## Quality gate7677- [ ] Every async class method and callback with multiple `setState` calls was classified as Category A, B, or C.78- [ ] Code after `await` was checked for `this.state` reads that drive decisions.79- [ ] `flushSync` was not used unless an intermediate UI state, spinner, progress step, or DOM measurement truly requires a synchronous render.80- [ ] Tests assert user-observable final behavior unless an intermediate state is deliberately visible.81- [ ] The relevant bundled reference was read when adding or rejecting `flushSync`.