feat: Adds React 18 and 19 migration plugin (#1339)

- Adds React 18 and 19 migration orchestration plugins
- Introduces comprehensive upgrade toolkits for migrating legacy React 16/17 and 18 codebases to React 18.3.1 and 19, respectively. Each plugin bundles specialized agents and skills for exhaustive audit, dependency management, class/component API migration, test suite transformation, and batching regression fixes.
- The React 18 toolkit targets class-component-heavy apps, ensures safe lifecycle and context transitions, resolves dependency blockers, and fully automates test migrations including Enzyme removal. The React 19 toolkit addresses breaking changes such as removal of legacy APIs, defaultProps on function components, and forwardRef, while enforcing a gated, memory-resumable migration pipeline.
- Both plugins update documentation, plugin registries, and skill references to support reliable, repeatable enterprise-scale React migrations.
This commit is contained in:
Saravanan Rajaraman
2026-04-09 10:48:52 +05:30
committed by GitHub
parent f4909cd581
commit 7f7b1b9b46
50 changed files with 8162 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
---
name: react18-batching-patterns
description: 'Provides exact patterns for diagnosing and fixing automatic batching regressions in React 18 class components. Use this skill whenever a class component has multiple setState calls in an async method, inside setTimeout, inside a Promise .then() or .catch(), or in a native event handler. Use it before writing any flushSync call - the decision tree here prevents unnecessary flushSync overuse. Also use this skill when fixing test failures caused by intermediate state assertions that break after React 18 upgrade.'
---
# React 18 Automatic Batching Patterns
Reference for diagnosing and fixing the most dangerous silent breaking change in React 18 for class-component codebases.
## The Core Change
| Location of setState | React 17 | React 18 |
|---|---|---|
| React event handler | Batched | Batched (same) |
| setTimeout | **Immediate re-render** | **Batched** |
| Promise .then() / .catch() | **Immediate re-render** | **Batched** |
| async/await | **Immediate re-render** | **Batched** |
| Native addEventListener callback | **Immediate re-render** | **Batched** |
**Batched** means: all setState calls within that execution context flush together in a single re-render at the end. No intermediate renders occur.
## Quick Diagnosis
Read every async class method. Ask: does any code after an `await` read `this.state` to make a decision?
```
Code reads this.state after await?
YES → Category A (silent state-read bug)
NO, but intermediate render must be visible to user?
YES → Category C (flushSync needed)
NO → Category B (refactor, no flushSync)
```
For the full pattern for each category, read:
- **`references/batching-categories.md`** - Category A, B, C with full before/after code
- **`references/flushSync-guide.md`** - when to use flushSync, when NOT to, import syntax
## The flushSync Rule
**Use `flushSync` sparingly.** It forces a synchronous re-render, bypassing React 18's concurrent scheduler. Overusing it negates the performance benefits of React 18.
Only use `flushSync` when:
- The user must see an intermediate UI state before an async operation begins
- A spinner/loading state must render before a fetch starts
- Sequential UI steps have distinct visible states (progress wizard, multi-step flow)
In most cases, the fix is a **refactor** - restructuring the code to not read `this.state` after `await`. Read `references/batching-categories.md` for the correct approach per category.

View File

@@ -0,0 +1,208 @@
# Batching Categories - Before/After Patterns
## Category A - this.state Read After Await (Silent Bug) {#category-a}
The method reads `this.state` after an `await` to make a conditional decision. In React 18, the intermediate setState hasn't flushed yet - `this.state` still holds the pre-update value.
**Before (broken in React 18):**
```jsx
async handleLoadClick() {
this.setState({ loading: true }); // batched - not flushed yet
const data = await fetchData();
if (this.state.loading) { // ← still FALSE (old value)
this.setState({ data, loading: false }); // ← never called
}
}
```
**After - remove the this.state read entirely:**
```jsx
async handleLoadClick() {
this.setState({ loading: true });
try {
const data = await fetchData();
this.setState({ data, loading: false }); // always called - no condition needed
} catch (err) {
this.setState({ error: err, loading: false });
}
}
```
**Pattern:** If the condition on `this.state` was always going to be true at that point (you just set it to true), remove the condition. The setState you called before `await` will eventually flush - you don't need to check it.
---
## Category A Variant - Multi-Step Conditional Chain
```jsx
// Before (broken):
async initialize() {
this.setState({ step: 'auth' });
const token = await authenticate();
if (this.state.step === 'auth') { // ← wrong: still initial value
this.setState({ step: 'loading', token });
const data = await loadData(token);
if (this.state.step === 'loading') { // ← wrong again
this.setState({ step: 'ready', data });
}
}
}
```
```jsx
// After - use local variables, not this.state, to track flow:
async initialize() {
this.setState({ step: 'auth' });
try {
const token = await authenticate();
this.setState({ step: 'loading', token });
const data = await loadData(token);
this.setState({ step: 'ready', data });
} catch (err) {
this.setState({ step: 'error', error: err });
}
}
```
---
## Category B - Independent setState Calls (Refactor, No flushSync) {#category-b}
Multiple setState calls in a Promise chain where order matters but no intermediate state reading occurs. The calls just need to be restructured.
**Before:**
```jsx
handleSubmit() {
this.setState({ submitting: true });
submitForm(this.state.formData)
.then(result => {
this.setState({ result });
this.setState({ submitting: false }); // two setState in .then()
});
}
```
**After - consolidate setState calls:**
```jsx
async handleSubmit() {
this.setState({ submitting: true, result: null, error: null });
try {
const result = await submitForm(this.state.formData);
this.setState({ result, submitting: false });
} catch (err) {
this.setState({ error: err, submitting: false });
}
}
```
Rule: Multiple `setState` calls in the same async context already batch in React 18. Consolidating into fewer calls is cleaner but not strictly required.
---
## Category C - Intermediate Render Must Be Visible (flushSync) {#category-c}
The user must see an intermediate UI state (loading spinner, progress step) BEFORE an async operation starts. This is the only case where `flushSync` is the right answer.
**Diagnostic question:** "If the loading spinner didn't appear until after the fetch returned, would the UX be wrong?"
- YES → `flushSync`
- NO → refactor (Category A or B)
**Before:**
```jsx
async processOrder() {
this.setState({ status: 'validating' }); // user must see this
await validateOrder(this.props.order);
this.setState({ status: 'charging' }); // user must see this
await chargeCard(this.props.card);
this.setState({ status: 'complete' });
}
```
**After - flushSync for each required intermediate render:**
```jsx
import { flushSync } from 'react-dom';
async processOrder() {
flushSync(() => {
this.setState({ status: 'validating' }); // renders immediately
});
await validateOrder(this.props.order);
flushSync(() => {
this.setState({ status: 'charging' }); // renders immediately
});
await chargeCard(this.props.card);
this.setState({ status: 'complete' }); // last - no flushSync needed
}
```
**Simple loading spinner case** (most common):
```jsx
import { flushSync } from 'react-dom';
async handleSearch() {
// User must see spinner before the fetch begins
flushSync(() => this.setState({ loading: true }));
const results = await searchAPI(this.state.query);
this.setState({ results, loading: false });
}
```
---
## setTimeout Pattern
```jsx
// Before (React 17 - setTimeout fired immediate re-renders):
handleAutoSave() {
setTimeout(() => {
this.setState({ saving: true });
// React 17: re-render happened here
saveToServer(this.state.formData).then(() => {
this.setState({ saving: false, lastSaved: Date.now() });
});
}, 2000);
}
```
```jsx
// After (React 18 - all setState inside setTimeout batches):
handleAutoSave() {
setTimeout(async () => {
// If loading state must show before fetch - flushSync
flushSync(() => this.setState({ saving: true }));
await saveToServer(this.state.formData);
this.setState({ saving: false, lastSaved: Date.now() });
}, 2000);
}
```
---
## Test Patterns That Break Due to Batching
```jsx
// Before (React 17 - intermediate state was synchronously visible):
it('shows saving indicator', () => {
render(<AutoSaveForm />);
fireEvent.change(input, { target: { value: 'new text' } });
expect(screen.getByText('Saving...')).toBeInTheDocument(); // ← sync check
});
// After (React 18 - use waitFor for intermediate states):
it('shows saving indicator', async () => {
render(<AutoSaveForm />);
fireEvent.change(input, { target: { value: 'new text' } });
await waitFor(() => expect(screen.getByText('Saving...')).toBeInTheDocument());
await waitFor(() => expect(screen.getByText('Saved')).toBeInTheDocument());
});
```

View File

@@ -0,0 +1,86 @@
# flushSync Guide
## Import
```jsx
import { flushSync } from 'react-dom';
// NOT from 'react' - it lives in react-dom
```
If the file already imports from `react-dom`:
```jsx
import ReactDOM from 'react-dom';
// Add named import:
import ReactDOM, { flushSync } from 'react-dom';
```
## Syntax
```jsx
flushSync(() => {
this.setState({ ... });
});
// After this line, the re-render has completed synchronously
```
Multiple setState calls inside one flushSync batch together into ONE synchronous render:
```jsx
flushSync(() => {
this.setState({ step: 'loading' });
this.setState({ progress: 0 });
// These batch together → one render
});
```
## When to Use
✅ Use when the user must see a specific UI state BEFORE an async operation starts:
```jsx
flushSync(() => this.setState({ loading: true }));
await expensiveAsyncOperation();
```
✅ Use in multi-step progress flows where each step must visually complete before the next:
```jsx
flushSync(() => this.setState({ status: 'validating' }));
await validate();
flushSync(() => this.setState({ status: 'processing' }));
await process();
```
✅ Use in tests that must assert an intermediate UI state synchronously (avoid when possible - prefer `waitFor`).
## When NOT to Use
❌ Don't use it to "fix" a reading-this.state-after-await bug - that's Category A (refactor instead):
```jsx
// WRONG - flushSync doesn't fix this
flushSync(() => this.setState({ loading: true }));
const data = await fetchData();
if (this.state.loading) { ... } // still a race condition
```
❌ Don't use it for every setState to "be safe" - it defeats React 18 concurrent rendering:
```jsx
// WRONG - excessive flushSync
async handleClick() {
flushSync(() => this.setState({ clicked: true })); // unnecessary
flushSync(() => this.setState({ processing: true })); // unnecessary
const result = await doWork();
flushSync(() => this.setState({ result, done: true })); // unnecessary
}
```
❌ Don't use it inside a `useEffect` or `componentDidMount` to trigger immediate state - it causes nested render cycles.
## Performance Note
`flushSync` forces a synchronous render, which blocks the browser thread until the render completes. On slow devices or complex component trees, multiple `flushSync` calls in an async method will cause visible jank. Use sparingly.
If you find yourself adding more than 2 `flushSync` calls to a single method, reconsider whether the component's state model needs redesign.