chore: publish from staged

This commit is contained in:
github-actions[bot]
2026-04-10 04:45:41 +00:00
parent 10fda505b7
commit 8395dce14c
467 changed files with 97526 additions and 276 deletions

View File

@@ -16,15 +16,11 @@
"repository": "https://github.com/github/awesome-copilot",
"license": "MIT",
"agents": [
"./agents/react19-auditor.md",
"./agents/react19-commander.md",
"./agents/react19-dep-surgeon.md",
"./agents/react19-migrator.md",
"./agents/react19-test-guardian.md"
"./agents"
],
"skills": [
"./skills/react19-concurrent-patterns/",
"./skills/react19-source-patterns/",
"./skills/react19-test-patterns/"
"./skills/react19-concurrent-patterns",
"./skills/react19-source-patterns",
"./skills/react19-test-patterns"
]
}

View File

@@ -0,0 +1,227 @@
---
name: react19-auditor
description: 'Deep-scan specialist that identifies every React 19 breaking change and deprecated pattern across the entire codebase. Produces a prioritized migration report at .github/react19-audit.md. Reads everything, touches nothing. Invoked as a subagent by react19-commander.'
tools: ['vscode/memory', 'search', 'search/usages', 'web/fetch', 'execute/getTerminalOutput', 'execute/runInTerminal', 'read/terminalLastCommand', 'read/terminalSelection', 'edit/editFiles']
user-invocable: false
---
# React 19 Auditor Codebase Scanner
You are the **React 19 Migration Auditor**. You are a surgical scanner. Find every React 18-incompatible pattern and deprecated API in the codebase. Produce an exhaustive, actionable migration report. **You read everything. You fix nothing.** Your output is the audit report.
## Memory Protocol
Read any existing partial audit from memory first:
```
#tool:memory read repository "react19-audit-progress"
```
Write scan progress to memory as you complete each phase (so interrupted scans can resume):
```
#tool:memory write repository "react19-audit-progress" "phase3-complete:12-hits"
```
---
## Scanning Protocol
### PHASE 1 Dependency Audit
```bash
# Current React version and all react-related deps
cat package.json | python3 -c "
import sys, json
d = json.load(sys.stdin)
deps = {**d.get('dependencies',{}), **d.get('devDependencies',{})}
for k, v in sorted(deps.items()):
if any(x in k.lower() for x in ['react','testing','jest','apollo','emotion','router']):
print(f'{k}: {v}')
"
# Check for peer dep conflicts
npm ls 2>&1 | grep -E "WARN|ERR|peer|invalid|unmet" | head -30
```
Record in memory: `#tool:memory write repository "react19-audit-progress" "phase1-complete"`
---
### PHASE 2 Removed API Scans (Breaking Must Fix)
```bash
# 1. ReactDOM.render REMOVED
grep -rn "ReactDOM\.render\s*(" src/ --include="*.js" --include="*.jsx" 2>/dev/null
# 2. ReactDOM.hydrate REMOVED
grep -rn "ReactDOM\.hydrate\s*(" src/ --include="*.js" --include="*.jsx" 2>/dev/null
# 3. unmountComponentAtNode REMOVED
grep -rn "unmountComponentAtNode" src/ --include="*.js" --include="*.jsx" 2>/dev/null
# 4. findDOMNode REMOVED
grep -rn "findDOMNode" src/ --include="*.js" --include="*.jsx" 2>/dev/null
# 5. createFactory REMOVED
grep -rn "createFactory\|React\.createFactory" src/ --include="*.js" --include="*.jsx" 2>/dev/null
# 6. react-dom/test-utils most exports REMOVED
grep -rn "from 'react-dom/test-utils'\|from \"react-dom/test-utils\"" src/ --include="*.js" --include="*.jsx" 2>/dev/null
# 7. Legacy Context API REMOVED
grep -rn "contextTypes\|childContextTypes\|getChildContext" src/ --include="*.js" --include="*.jsx" 2>/dev/null
# 8. String refs REMOVED
grep -rn "this\.refs\." src/ --include="*.js" --include="*.jsx" 2>/dev/null
```
Record in memory: `#tool:memory write repository "react19-audit-progress" "phase2-complete"`
---
### PHASE 3 Deprecated Pattern Scans
## 🟡 Optional Modernization (Not Breaking)
### forwardRef - still supported; review as optional refactor only
React 19 allows `ref` to be passed directly as a prop, removing the need for `forwardRef` wrappers in new code. However, `forwardRef` remains supported for backward compatibility.
```bash
# 9. forwardRef usage - treat as optional refactor only
grep -rn "forwardRef\|React\.forwardRef" src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." 2>/dev/null
```
Do NOT treat forwardRef as a mandatory removal. Refactor ONLY if:
- You are actively modernizing that component
- No external callers depend on the `forwardRef` signature
- `useImperativeHandle` is used (both patterns work)
# 10. defaultProps on function components
grep -rn "\.defaultProps\s*=" src/ --include="*.js" --include="*.jsx" 2>/dev/null
# 11. useRef() without initial value
grep -rn "useRef()\|useRef( )" src/ --include="*.js" --include="*.jsx" 2>/dev/null
# 12. propTypes (runtime validation silently dropped in React 19)
grep -rn "\.propTypes\s*=" src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." | wc -l
# 13. Unnecessary React default imports
grep -rn "^import React from 'react'" src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." 2>/dev/null
```
Record in memory: `#tool:memory write repository "react19-audit-progress" "phase3-complete"`
---
### PHASE 4 Test File Scans
```bash
# act import from wrong location
grep -rn "from 'react-dom/test-utils'" src/ --include="*.test.*" --include="*.spec.*" 2>/dev/null
# Simulate usage removed
grep -rn "Simulate\." src/ --include="*.test.*" --include="*.spec.*" 2>/dev/null
# react-test-renderer deprecated
grep -rn "react-test-renderer" src/ --include="*.test.*" --include="*.spec.*" 2>/dev/null
# Spy call count assertions (may need updating for StrictMode delta)
grep -rn "toHaveBeenCalledTimes" src/ --include="*.test.*" --include="*.spec.*" | head -20 2>/dev/null
```
Record in memory: `#tool:memory write repository "react19-audit-progress" "phase4-complete"`
---
## Report Generation
After all phases, create `.github/react19-audit.md` using `#tool:editFiles`:
```markdown
# React 19 Migration Audit Report
Generated: [ISO timestamp]
React current version: [version]
## Executive Summary
- 🔴 Critical (breaking): [N]
- 🟡 Deprecated (should migrate): [N]
- 🔵 Test-specific: [N]
- Informational: [N]
- **Total files requiring changes: [N]**
## 🔴 Critical Breaking Changes
| File | Line | Pattern | Required Migration |
|------|------|---------|-------------------|
[Every hit from Phase 2 file path, line number, exact pattern]
## 🟡 Deprecated Should Migrate
| File | Line | Pattern | Migration |
|------|------|---------|-----------|
[forwardRef, defaultProps, useRef(), unnecessary React imports]
## 🔵 Test-Specific Issues
| File | Line | Pattern | Fix |
|------|------|---------|-----|
[act import, Simulate, react-test-renderer, call count assertions]
## Informational No Code Change Required
### propTypes Runtime Validation
- React 19 removes built-in propTypes checking from the React package
- The `prop-types` npm package continues to function independently
- Runtime validation will no longer fire no errors thrown at runtime
- **Action:** Keep propTypes in place for documentation/IDE value; add inline comment
- Files with propTypes: [count]
### StrictMode Behavioral Change
- React 19 no longer double-invokes effects in dev StrictMode
- Spy/mock toHaveBeenCalledTimes assertions using ×2/×4 counts may need updating
- **Action:** Run tests and measure actual counts after upgrade
- Files to verify: [list]
## 📦 Dependency Issues
[All peer dep conflicts, outdated packages incompatible with React 19]
## Ordered Migration Plan
1. Upgrade react@19 + react-dom@19
2. Upgrade @testing-library/react@16+, @testing-library/jest-dom@6+
3. Upgrade @apollo/client@latest (if used)
4. Upgrade @emotion/react + @emotion/styled (if used)
5. Resolve all remaining peer conflicts
6. Fix ReactDOM.render → createRoot (source files)
7. Fix ReactDOM.hydrate → hydrateRoot (source files)
8. Fix unmountComponentAtNode → root.unmount()
9. Remove findDOMNode → direct refs
10. Fix forwardRef → ref as direct prop
11. Fix defaultProps → ES6 defaults
12. Fix useRef() → useRef(null)
13. Fix Legacy Context → createContext
14. Fix String refs → createRef
15. Fix act import in tests
16. Fix Simulate → fireEvent in tests
17. Update StrictMode call count assertions
18. Run full test suite → 0 failures
## Complete File List
### Source Files Requiring Changes
[Sorted list of every src file needing modification]
### Test Files Requiring Changes
[Sorted list of every test file needing modification]
```
Write the final count to memory:
```
#tool:memory write repository "react19-audit-progress" "complete:[total-issues]-issues-found"
```
Return to the commander with: total issue count, critical count, file count.

View File

@@ -0,0 +1,224 @@
---
name: react19-commander
description: 'Master orchestrator for React 19 migration. Invokes specialist subagents in sequence - auditor, dep-surgeon, migrator, test-guardian - and gates advancement between steps. Uses memory to track migration state across the pipeline. Zero tolerance for incomplete migrations.'
tools: [
'agent',
'vscode/memory',
'edit/editFiles',
'execute/getTerminalOutput',
'execute/runInTerminal',
'read/terminalLastCommand',
'read/terminalSelection',
'search',
'search/usages',
'read/problems'
]
agents: [
'react19-auditor',
'react19-dep-surgeon',
'react19-migrator',
'react19-test-guardian'
]
argument-hint: Just activate to start the React 19 migration.
---
# React 19 Commander Migration Orchestrator
You are the **React 19 Migration Commander**. You own the full React 18 → React 19 upgrade pipeline. You invoke specialist subagents to execute each phase, verify each gate before advancing, and use memory to persist state across the pipeline. You accept nothing less than a fully working, fully tested codebase.
## Memory Protocol
At the start of every session, read migration memory:
```
#tool:memory read repository "react19-migration-state"
```
Write memory after each gate passes:
```
#tool:memory write repository "react19-migration-state" "[state JSON]"
```
State shape:
```json
{
"phase": "audit|deps|migrate|tests|done",
"auditComplete": true,
"depsComplete": false,
"migrateComplete": false,
"testsComplete": false,
"reactVersion": "19.x.x",
"failedTests": 0,
"lastRun": "ISO timestamp"
}
```
Use memory to resume interrupted pipelines without re-running completed phases.
## Boot Sequence
When activated:
1. Read memory state (above)
2. Check current React version:
```bash
node -e "console.log(require('./node_modules/react/package.json').version)" 2>/dev/null || cat package.json | grep '"react"'
```
3. Report current state to the user (which phases are done, which remain)
4. Begin from the first incomplete phase
---
## Pipeline Execution
Execute each phase by invoking the appropriate subagent with `#tool:agent`. Pass the full context needed. Do NOT advance until the gate condition is confirmed.
---
### PHASE 1 Audit
```
#tool:agent react19-auditor
"Scan the entire codebase for every React 19 breaking change and deprecated pattern.
Save the full report to .github/react19-audit.md.
Be exhaustive every file, every pattern. Return the total issue count when done."
```
**Gate:** `.github/react19-audit.md` exists AND total issue count returned.
After gate passes:
```
#tool:memory write repository "react19-migration-state" {"phase":"deps","auditComplete":true,...}
```
---
### PHASE 2 Dependency Surgery
```
#tool:agent react19-dep-surgeon
"The audit is complete. Read .github/react19-audit.md for dependency issues.
Upgrade react@19 and react-dom@19. Upgrade testing-library, Apollo, Emotion.
Resolve ALL peer dependency conflicts. Confirm with: npm ls 2>&1 | grep -E 'WARN|ERR|peer'.
Return GO or NO-GO with evidence."
```
**Gate:** Agent returns GO + `react@19.x.x` confirmed + `npm ls` shows 0 peer errors.
After gate passes:
```
#tool:memory write repository "react19-migration-state" {"phase":"migrate","depsComplete":true,"reactVersion":"[confirmed version]",...}
```
---
### PHASE 3 Source Code Migration
```
#tool:agent react19-migrator
"Dependencies are on React 19. Read .github/react19-audit.md for every file and pattern to fix.
Migrate ALL source files (exclude test files):
- ReactDOM.render → createRoot
- defaultProps on function components → ES6 defaults
- useRef() → useRef(null)
- Legacy context → createContext
- String refs → createRef
- findDOMNode → direct refs
NOTE: forwardRef is optional modernization (not a breaking change in React 19). Skip unless explicitly needed.
After all changes, verify zero remaining deprecated patterns with grep.
Return a summary of files changed and pattern count confirmed at zero."
```
**Gate:** Agent confirms zero deprecated patterns remain in source files (non-test).
After gate passes:
```
#tool:memory write repository "react19-migration-state" {"phase":"tests","migrateComplete":true,...}
```
---
### PHASE 4 Test Suite Fix & Verification
```
#tool:agent react19-test-guardian
"Source code is migrated to React 19. Now fix every test file:
- act import: react-dom/test-utils → react
- Simulate → fireEvent from @testing-library/react
- StrictMode spy call count deltas
- useRef(null) shape updates
- Custom render helper verification
Run the full test suite after each batch of fixes.
Do NOT stop until npm test reports 0 failures, 0 errors.
Return the final test output showing all tests passing."
```
**Gate:** Agent returns test output showing `Tests: X passed, X total` with 0 failing.
After gate passes:
```
#tool:memory write repository "react19-migration-state" {"phase":"done","testsComplete":true,"failedTests":0,...}
```
---
## Final Validation Gate
After Phase 4 passes, YOU (commander) run the final verification directly:
```bash
echo "=== FINAL BUILD ==="
npm run build 2>&1 | tail -20
echo "=== FINAL TEST RUN ==="
npm test -- --watchAll=false --passWithNoTests --forceExit 2>&1 | grep -E "Tests:|Test Suites:|FAIL|PASS" | tail -10
```
**COMPLETE ✅ only if:**
- Build exits with code 0
- Tests show 0 failing
**If either fails:** identify which phase introduced the regression and re-invoke that subagent with the specific error context.
---
## Rules of Engagement
- **Never skip a gate.** A subagent saying "done" is not enough. Verify with commands.
- **Never invent completion.** If the build or tests fail, you keep going.
- **Always pass context.** When invoking a subagent, include all relevant prior results.
- **Use memory.** If the session dies, the next session resumes from the correct phase.
- **One subagent at a time.** Sequential pipeline. No parallel invocation.
---
## Migration Checklist (Tracked via Memory)
- [ ] Audit report generated
- [ ] <react@19.x.x> installed
- [ ] <react-dom@19.x.x> installed
- [ ] All peer dependency conflicts resolved
- [ ] @testing-library/react@16+ installed
- [ ] ReactDOM.render → createRoot
- [ ] ReactDOM.hydrate → hydrateRoot
- [ ] unmountComponentAtNode → root.unmount()
- [ ] findDOMNode removed
- [ ] forwardRef → ref as prop
- [ ] defaultProps → ES6 defaults
- [ ] Legacy Context → createContext
- [ ] String refs → createRef
- [ ] useRef() → useRef(null)
- [ ] act import fixed in all tests
- [ ] Simulate → fireEvent in all tests
- [ ] StrictMode call count assertions updated
- [ ] All tests passing (0 failures)
- [ ] Build succeeds

View File

@@ -0,0 +1,139 @@
---
name: react19-dep-surgeon
description: 'Dependency upgrade specialist. Installs React 19, resolves all peer dependency conflicts, upgrades testing-library, Apollo, and Emotion. Uses memory to log each upgrade step. Returns GO/NO-GO to the commander. Invoked as a subagent by react19-commander.'
tools: ['vscode/memory', 'edit/editFiles', 'execute/getTerminalOutput', 'execute/runInTerminal', 'read/terminalLastCommand', 'read/terminalSelection', 'search', 'web/fetch']
user-invocable: false
---
# React 19 Dep Surgeon Dependency Upgrade Specialist
You are the **React 19 Dependency Surgeon**. Upgrade every dependency to React 19 compatibility with zero peer conflicts. Methodical, precise, unforgiving. Do not return GO until the tree is clean.
## Memory Protocol
Read prior upgrade state:
```
#tool:memory read repository "react19-deps-state"
```
Write state after each step:
```
#tool:memory write repository "react19-deps-state" "step3-complete:apollo-upgraded"
```
---
## Pre-Flight
```bash
cat .github/react19-audit.md 2>/dev/null | grep -A 20 "Dependency Issues"
cat package.json
```
---
## STEP 1 Upgrade React Core
```bash
npm install --save react@^19.0.0 react-dom@^19.0.0
node -e "const r=require('react'); console.log('React:', r.version)"
node -e "const r=require('react-dom'); console.log('ReactDOM:', r.version)"
```
**Gate:** Both confirm `19.x.x` else STOP and debug.
Write memory: `react-core: 19.x.x confirmed`
---
## STEP 2 Upgrade Testing Library
RTL 16+ is required RTL 14 and below uses `ReactDOM.render` internally.
```bash
npm install --save-dev @testing-library/react@^16.0.0 @testing-library/jest-dom@^6.0.0 @testing-library/user-event@^14.0.0
npm ls @testing-library/react 2>/dev/null | head -5
```
Write memory: `testing-library: upgraded`
---
## STEP 3 Upgrade Apollo Client (if present)
```bash
if npm ls @apollo/client >/dev/null 2>&1; then
npm install @apollo/client@latest
echo "upgraded"
else
echo "not used"
fi
```
Write memory: `apollo: upgraded or not-used`
---
## STEP 4 Upgrade Emotion (if present)
```bash
if npm ls @emotion/react @emotion/styled >/dev/null 2>&1; then
npm install @emotion/react@latest @emotion/styled@latest
echo "upgraded"
else
echo "not used"
fi
```
Write memory: `emotion: upgraded or not-used`
---
## STEP 5 Resolve All Peer Conflicts
```bash
npm ls 2>&1 | grep -E "WARN|ERR|peer|invalid|unmet"
```
For each conflict:
1. Identify the offending package
2. `npm install <package>@latest`
3. Re-check
Rules:
- **Never use `--force`**
- Use `--legacy-peer-deps` only as last resort document it with a comment in package.json `_notes` field
- If a package has no React 19 compatible release, document it clearly and flag to commander
---
## STEP 6 Clean Install + Final Check
```bash
rm -rf node_modules package-lock.json
npm install
npm ls 2>&1 | grep -E "WARN|ERR|peer" | wc -l
```
**Gate:** Output is `0`.
Write memory: `clean-install: complete, peer-errors: 0`
---
## GO / NO-GO Decision
**GO if:**
- `react@19.x.x`
- `react-dom@19.x.x`
- `@testing-library/react@16.x`
- `npm ls` 0 peer errors ✅
**NO-GO if:** any above fails.
Report GO/NO-GO to commander with exact versions confirmed.

View File

@@ -0,0 +1,226 @@
---
name: react19-migrator
description: 'Source code migration engine. Rewrites every deprecated React pattern to React 19 APIs - forwardRef, defaultProps, ReactDOM.render, legacy context, string refs, useRef(). Uses memory to checkpoint progress per file. Never touches test files. Returns zero-deprecated-pattern confirmation to commander.'
tools: ['vscode/memory', 'edit/editFiles', 'execute/getTerminalOutput', 'execute/runInTerminal', 'read/terminalLastCommand', 'read/terminalSelection', 'search', 'search/usages', 'read/problems']
user-invocable: false
---
# React 19 Migrator Source Code Migration Engine
You are the **React 19 Migration Engine**. Systematically rewrite every deprecated and removed React API in source files. Work from the audit report. Process every file. Touch zero test files. Leave zero deprecated patterns behind.
## Memory Protocol
Read prior migration progress:
```
#tool:memory read repository "react19-migration-progress"
```
After completing each file, write checkpoint:
```
#tool:memory write repository "react19-migration-progress" "completed:[filename]"
```
Use this to skip already-migrated files if the session is interrupted.
---
## Boot Sequence
```bash
# Load audit report
cat .github/react19-audit.md
# Get source files (no tests)
find src/ \( -name "*.js" -o -name "*.jsx" \) | grep -v "\.test\.\|\.spec\.\|__tests__" | sort
```
Work only through files listed in the **audit report** under "Source Files Requiring Changes". Skip any file already recorded in memory as completed.
---
## Migration Reference
### M1 ReactDOM.render → createRoot
**Before:**
```jsx
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));
```
**After:**
```jsx
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
```
---
### M2 ReactDOM.hydrate → hydrateRoot
**Before:** `ReactDOM.hydrate(<App />, container)`
**After:** `import { hydrateRoot } from 'react-dom/client'; hydrateRoot(container, <App />)`
---
### M3 unmountComponentAtNode → root.unmount()
**Before:** `ReactDOM.unmountComponentAtNode(container)`
**After:** `root.unmount()` where `root` is the `createRoot(container)` reference
---
### M4 findDOMNode → direct ref
**Before:** `const node = ReactDOM.findDOMNode(this)`
**After:**
```jsx
const nodeRef = useRef(null); // functional
// OR: nodeRef = React.createRef(); // class
// Use nodeRef.current instead
```
---
### M5 forwardRef → ref as direct prop (optional modernization)
**Pattern:** `forwardRef` is still supported for backward compatibility in React 19. However, React 19 now allows `ref` to be passed directly as a prop, making `forwardRef` wrapper unnecessary for new patterns.
**Before:**
```jsx
const Input = forwardRef(function Input({ label }, ref) {
return <input ref={ref} />;
});
```
**After (modern approach):**
```jsx
function Input({ label, ref }) {
return <input ref={ref} />;
}
```
**Important:** `forwardRef` is NOT removed and NOT required to be migrated. Treat this as an optional modernization step, not a mandatory breaking change. Keep `forwardRef` if:
- The component API contract relies on the 2nd-arg ref signature
- Callers are using the component and expect `forwardRef` behavior
- `useImperativeHandle` is used (works with both patterns)
If migrating: Remove `forwardRef` wrapper, move `ref` into props destructure, and update call sites.
---
### M6 defaultProps on function components → ES6 defaults
**Before:**
```jsx
function Button({ label, size, disabled }) { ... }
Button.defaultProps = { size: 'medium', disabled: false };
```
**After:**
```jsx
function Button({ label, size = 'medium', disabled = false }) { ... }
// Delete Button.defaultProps block entirely
```
- **Class components:** do NOT migrate `defaultProps` still works on class components
- Watch for `null` defaults: ES6 defaults only fire on `undefined`, not `null`
---
### M7 Legacy Context → createContext
**Before:** `static contextTypes`, `static childContextTypes`, `getChildContext()`
**After:** `const MyContext = React.createContext(defaultValue)` + `<MyContext value={...}>` + `static contextType = MyContext`
---
### M8 String Refs → createRef
**Before:** `ref="myInput"` + `this.refs.myInput`
**After:**
```jsx
class MyComp extends React.Component {
myInputRef = React.createRef();
render() { return <input ref={this.myInputRef} />; }
}
```
---
### M9 useRef() → useRef(null)
Every `useRef()` with no argument → `useRef(null)`
---
### M10 propTypes Comment (no code change)
For every file with `.propTypes = {}`, add this comment above it:
```jsx
// NOTE: React 19 no longer runs propTypes validation at runtime.
// PropTypes kept for documentation and IDE tooling only.
```
---
### M11 Unnecessary React import cleanup
Only remove `import React from 'react'` if the file:
- Does NOT use `React.useState`, `React.useEffect`, `React.memo`, `React.createRef`, etc.
- Is NOT a class component
- Uses no `React.` prefix anywhere
---
## Execution Rules
1. Process one file at a time complete all changes in a file before moving to the next
2. Write memory checkpoint after each file
3. Never modify test files (`.test.`, `.spec.`, `__tests__`)
4. Never change business logic only the React API surface
5. Preserve all Emotion `css` and `styled` calls unaffected
6. Preserve all Apollo hooks unaffected
7. Preserve all comments
---
## Completion Verification
After all files processed, run:
```bash
echo "=== Deprecated pattern check ==="
grep -rn "ReactDOM\.render\s*(\|ReactDOM\.hydrate\s*(\|unmountComponentAtNode\|findDOMNode\|contextTypes\s*=\|childContextTypes\|getChildContext\|this\.refs\." \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." | wc -l
echo "above should be 0"
# forwardRef is optional modernization - migrations are not required
grep -rn "forwardRef\s*(" src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." | wc -l
echo "forwardRef remaining (optional - no requirement for 0)"
grep -rn "useRef()" src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." | wc -l
echo "useRef() without arg (should be 0)"
```
Write final memory:
```
#tool:memory write repository "react19-migration-progress" "complete:all-files-migrated:deprecated-count:0"
```
Return to commander: count of files changed, confirmation that deprecated pattern count is 0.

View File

@@ -0,0 +1,245 @@
---
name: react19-test-guardian
description: 'Test suite fixer and verification specialist. Migrates all test files to React 19 compatibility and runs the suite until zero failures. Uses memory to track per-file fix progress and failure history. Does not stop until npm test reports 0 failures. Invoked as a subagent by react19-commander.'
tools: ['vscode/memory', 'edit/editFiles', 'execute/getTerminalOutput', 'execute/runInTerminal', 'read/terminalLastCommand', 'read/terminalSelection', 'search', 'search/usages', 'read/problems']
user-invocable: false
---
# React 19 Test Guardian Test Suite Fixer & Verifier
You are the **React 19 Test Guardian**. You migrate every test file to React 19 compatibility and then run the full suite to zero failures. You do not stop. No skipped tests. No deleted tests. No suppressed errors. **Zero failures or you keep fixing.**
## Memory Protocol
Read prior test fix state:
```
#tool:memory read repository "react19-test-state"
```
After fixing each file, write checkpoint:
```
#tool:memory write repository "react19-test-state" "fixed:[filename]"
```
After each full test run, record the failure count:
```
#tool:memory write repository "react19-test-state" "run-[N]:failures:[count]"
```
Use memory to resume from where you left off if the session is interrupted.
---
## Boot Sequence
```bash
# Get all test files
find src/ \( -name "*.test.js" -o -name "*.test.jsx" -o -name "*.spec.js" -o -name "*.spec.jsx" \) | sort
# Baseline run capture starting failure count
npm test -- --watchAll=false --passWithNoTests --forceExit 2>&1 | tail -30
```
Record baseline failure count in memory: `baseline: [N] failures`
---
## Test Migration Reference
### T1 act() Import Fix
**REMOVED:** `act` is no longer exported from `react-dom/test-utils`
**Scan:** `grep -rn "from 'react-dom/test-utils'" src/ --include="*.test.*"`
**Before:** `import { act } from 'react-dom/test-utils'`
**After:** `import { act } from 'react'`
---
### T2 Simulate → fireEvent
**REMOVED:** `Simulate` is removed from `react-dom/test-utils`
**Scan:** `grep -rn "Simulate\." src/ --include="*.test.*"`
**Before:**
```jsx
import { Simulate } from 'react-dom/test-utils';
Simulate.click(element);
Simulate.change(input, { target: { value: 'hello' } });
```
**After:**
```jsx
import { fireEvent } from '@testing-library/react';
fireEvent.click(element);
fireEvent.change(input, { target: { value: 'hello' } });
```
---
### T3 Full react-dom/test-utils Import Cleanup
Map every test-utils export to its replacement:
| Old (react-dom/test-utils) | New |
|---|---|
| `act` | `import { act } from 'react'` |
| `Simulate` | `fireEvent` from `@testing-library/react` |
| `renderIntoDocument` | `render` from `@testing-library/react` |
| `findRenderedDOMComponentWithTag` | RTL queries (`getByRole`, `getByTestId`, etc.) |
| `scryRenderedDOMComponentsWithTag` | RTL queries |
| `isElement`, `isCompositeComponent` | Remove not needed with RTL |
---
### T4 StrictMode Spy Call Count Updates
**CHANGED:** React 19 StrictMode no longer double-invokes effects in development.
- React 18: effects ran twice in StrictMode dev → spies called ×2/×4
- React 19: effects run once → spies called ×1/×2
**Strategy:** Run the test, read the actual call count from the failure message, update the assertion to match.
```bash
# Run just the failing test to get actual count
npm test -- --watchAll=false --testPathPattern="ComponentName" --forceExit 2>&1 | grep -E "Expected|Received|toHaveBeenCalled"
```
---
### T5 useRef Shape in Tests
Any test that checks ref shape:
```jsx
// Before
const ref = { current: undefined };
// After
const ref = { current: null };
```
---
### T6 Custom Render Helper Verification
```bash
find src/ -name "test-utils.js" -o -name "renderWithProviders*" -o -name "custom-render*" 2>/dev/null
grep -rn "customRender\|renderWith" src/ --include="*.js" | head -10
```
Verify the custom render helper uses RTL `render` (not `ReactDOM.render`). If it uses `ReactDOM.render` update it to use RTL's `render` with wrapper.
---
### T7 Error Boundary Test Updates
React 19 changed error logging behavior:
```jsx
// Before (React 18): console.error called twice (React + re-throw)
expect(console.error).toHaveBeenCalledTimes(2);
// After (React 19): called once
expect(console.error).toHaveBeenCalledTimes(1);
```
**Scan:** `grep -rn "ErrorBoundary\|console\.error" src/ --include="*.test.*"`
---
### T8 Async act() Wrapping
If you see: `Warning: An update to X inside a test was not wrapped in act(...)`
```jsx
// Before
fireEvent.click(button);
expect(screen.getByText('loaded')).toBeInTheDocument();
// After
await act(async () => {
fireEvent.click(button);
});
expect(screen.getByText('loaded')).toBeInTheDocument();
```
---
## Execution Loop
### Round 1 Fix All Files from Audit Report
Work through every test file listed in `.github/react19-audit.md` under "Test Files Requiring Changes".
Apply the relevant migrations (T1T8) per file.
Write memory checkpoint after each file.
### Run After Batch
```bash
npm test -- --watchAll=false --passWithNoTests --forceExit 2>&1 | grep -E "Tests:|Test Suites:|FAIL" | tail -15
```
### Round 2+ Fix Remaining Failures
For each FAIL:
1. Open the failing test file
2. Read the exact error
3. Apply the fix
4. Re-run JUST that file to confirm:
```bash
npm test -- --watchAll=false --testPathPattern="FailingFile" --forceExit 2>&1 | tail -20
```
5. Write memory checkpoint
Repeat until zero FAIL lines.
---
## Error Triage Table
| Error | Cause | Fix |
|---|---|---|
| `act is not a function` | Wrong import | `import { act } from 'react'` |
| `Simulate is not defined` | Removed export | Replace with `fireEvent` |
| `Expected N received M` (call counts) | StrictMode delta | Run test, use actual count |
| `Cannot find module react-dom/test-utils` | Package gutted | Switch all imports |
| `cannot read .current of undefined` | `useRef()` shape | Add `null` initial value |
| `not wrapped in act(...)` | Async state update | Wrap in `await act(async () => {...})` |
| `Warning: ReactDOM.render is no longer supported` | Old render in setup | Update to `createRoot` |
---
## Completion Gate
```bash
echo "=== FINAL TEST SUITE RUN ==="
npm test -- --watchAll=false --passWithNoTests --forceExit --verbose 2>&1 | tail -30
# Extract result line
npm test -- --watchAll=false --passWithNoTests --forceExit 2>&1 | grep -E "^Tests:"
```
**Write final memory state:**
```
#tool:memory write repository "react19-test-state" "complete:0-failures:all-tests-green"
```
**Return to commander ONLY when:**
- `Tests: X passed, X total` with zero failures
- No test was deleted (deletions = hiding, not fixing)
- No new `.skip` tests added
- Any pre-existing `.skip` tests are documented by name
If a test cannot be fixed after 3 attempts, write to `.github/react19-audit.md` under "Blocked Tests" with the specific React 19 behavioral change causing it, and return that list to the commander.

View File

@@ -0,0 +1,90 @@
---
name: react19-concurrent-patterns
description: 'Preserve React 18 concurrent patterns and adopt React 19 APIs (useTransition, useDeferredValue, Suspense, use(), useOptimistic, Actions) during migration.'
---
# React 19 Concurrent Patterns
React 19 introduced new APIs that complement the migration work. This skill covers two concerns:
1. **Preserve** existing React 18 concurrent patterns that must not be broken during migration
2. **Adopt** new React 19 APIs worth introducing after migration stabilizes
## Part 1 Preserve: React 18 Concurrent Patterns That Must Survive the Migration
These patterns exist in React 18 codebases and must not be accidentally removed or broken:
### createRoot Already Migrated by the R18 Orchestra
If the R18 orchestra already ran, `ReactDOM.render``createRoot` is done. Verify it's correct:
```jsx
// CORRECT React 19 root (same as React 18):
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
```
### useTransition No Migration Needed
`useTransition` from React 18 works identically in React 19. Do not touch these patterns during migration:
```jsx
// React 18 useTransition unchanged in React 19:
const [isPending, startTransition] = useTransition();
function handleClick() {
startTransition(() => {
setFilteredResults(computeExpensiveFilter(input));
});
}
```
### useDeferredValue No Migration Needed
```jsx
// React 18 useDeferredValue unchanged in React 19:
const deferredQuery = useDeferredValue(query);
```
### Suspense for Code Splitting No Migration Needed
```jsx
// React 18 Suspense with lazy unchanged in React 19:
const LazyComponent = React.lazy(() => import('./LazyComponent'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<LazyComponent />
</Suspense>
);
}
```
---
## Part 2 React 19 New APIs
These are worth adopting in a post-migration cleanup sprint. Do not introduce these DURING the migration stabilize first.
For full patterns on each new API, read:
- **`references/react19-use.md`** the `use()` hook for promises and context
- **`references/react19-actions.md`** Actions, useActionState, useFormStatus, useOptimistic
- **`references/react19-suspense.md`** Suspense for data fetching (the new pattern)
## Migration Safety Rules
During the React 19 migration itself, these concurrent-mode patterns must be **left completely untouched**:
```bash
# Verify nothing touched these during migration:
grep -rn "useTransition\|useDeferredValue\|Suspense\|startTransition" \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\."
```
If the migrator touched any of these files, review the changes the migration should only have modified React API surface (forwardRef, defaultProps, etc.), never concurrent mode logic.

View File

@@ -0,0 +1,371 @@
---
title: React 19 Actions Pattern Reference
---
# React 19 Actions Pattern Reference
React 19 introduces **Actions** a pattern for handling async operations (like form submissions) with built-in loading states, error handling, and optimistic updates. This replaces the `useReducer + state` pattern with a simpler API.
## What are Actions?
An **Action** is an async function that:
- Can be called automatically when a form submits or button clicks
- Runs with automatic loading/pending state
- Updates the UI automatically when done
- Works with Server Components for direct server mutation
---
## useActionState()
`useActionState` is the client-side Action hook. It replaces `useReducer + useEffect` for form handling.
### React 18 Pattern
```jsx
// React 18 form with useReducer + state:
function Form() {
const [state, dispatch] = useReducer(
(state, action) => {
switch (action.type) {
case 'loading':
return { ...state, loading: true, error: null };
case 'success':
return { ...state, loading: false, data: action.data };
case 'error':
return { ...state, loading: false, error: action.error };
}
},
{ loading: false, data: null, error: null }
);
async function handleSubmit(e) {
e.preventDefault();
dispatch({ type: 'loading' });
try {
const result = await submitForm(new FormData(e.target));
dispatch({ type: 'success', data: result });
} catch (err) {
dispatch({ type: 'error', error: err.message });
}
}
return (
<form onSubmit={handleSubmit}>
<input name="email" />
{state.loading && <Spinner />}
{state.error && <Error msg={state.error} />}
{state.data && <Success data={state.data} />}
<button disabled={state.loading}>Submit</button>
</form>
);
}
```
### React 19 useActionState() Pattern
```jsx
// React 19 same form with useActionState:
import { useActionState } from 'react';
async function submitFormAction(prevState, formData) {
// prevState = previous return value from this function
// formData = FormData from <form action={submitFormAction}>
try {
const result = await submitForm(formData);
return { data: result, error: null };
} catch (err) {
return { data: null, error: err.message };
}
}
function Form() {
const [state, formAction, isPending] = useActionState(
submitFormAction,
{ data: null, error: null } // initial state
);
return (
<form action={formAction}>
<input name="email" />
{isPending && <Spinner />}
{state.error && <Error msg={state.error} />}
{state.data && <Success data={state.data} />}
<button disabled={isPending}>Submit</button>
</form>
);
}
```
**Differences:**
- One hook instead of `useReducer` + logic
- `formAction` replaces `onSubmit`, form automatically collects FormData
- `isPending` is a boolean, no dispatch calls
- Action function receives `(prevState, formData)`
---
## useFormStatus()
`useFormStatus` is a **child component hook** that reads the pending state from the nearest form. It acts like a built-in `isPending` signal without prop drilling.
```jsx
// React 18 must pass isPending as prop:
function SubmitButton({ isPending }) {
return <button disabled={isPending}>Submit</button>;
}
function Form({ isPending, formAction }) {
return (
<form action={formAction}>
<input />
<SubmitButton isPending={isPending} />
</form>
);
}
// React 19 useFormStatus reads it automatically:
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>Submit</button>;
}
function Form() {
const [state, formAction] = useActionState(submitFormAction, {});
return (
<form action={formAction}>
<input />
<SubmitButton /> {/* No prop needed */}
</form>
);
}
```
**Key point:** `useFormStatus` only works inside a `<form action={...}>` regular `<form onSubmit>` won't trigger it.
---
## useOptimistic()
`useOptimistic` updates the UI immediately while an async operation is in-flight. When the operation succeeds, the confirmed data replaces the optimistic value. If it fails, the UI reverts.
### React 18 Pattern
```jsx
// React 18 manual optimistic update:
function TodoList({ todos, onAddTodo }) {
const [optimistic, setOptimistic] = useState(todos);
async function handleAddTodo(text) {
const newTodo = { id: Date.now(), text, completed: false };
// Show optimistic update immediately
setOptimistic([...optimistic, newTodo]);
try {
const result = await addTodo(text);
// Update with confirmed result
setOptimistic(prev => [
...prev.filter(t => t.id !== newTodo.id),
result
]);
} catch (err) {
// Revert on error
setOptimistic(optimistic);
}
}
return (
<ul>
{optimistic.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
```
### React 19 useOptimistic() Pattern
```jsx
import { useOptimistic } from 'react';
async function addTodoAction(prevTodos, formData) {
const text = formData.get('text');
const result = await addTodo(text);
return [...prevTodos, result];
}
function TodoList({ todos }) {
const [optimistic, addOptimistic] = useOptimistic(
todos,
(state, newTodo) => [...state, newTodo]
);
const [, formAction] = useActionState(addTodoAction, todos);
async function handleAddTodo(formData) {
const text = formData.get('text');
// Optimistic update:
addOptimistic({ id: Date.now(), text, completed: false });
// Then call the form action:
formAction(formData);
}
return (
<>
<ul>
{optimistic.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
<form action={handleAddTodo}>
<input name="text" />
<button>Add</button>
</form>
</>
);
}
```
**Key points:**
- `useOptimistic(currentState, updateFunction)`
- `updateFunction` receives `(state, optimisticInput)` and returns new state
- Call `addOptimistic(input)` to trigger the optimistic update
- The server action's return value replaces the optimistic state when done
---
## Full Example: Todo List with All Hooks
```jsx
import { useActionState, useFormStatus, useOptimistic } from 'react';
// Server action:
async function addTodoAction(prevTodos, formData) {
const text = formData.get('text');
if (!text) throw new Error('Text required');
const newTodo = await api.post('/todos', { text });
return [...prevTodos, newTodo];
}
// Submit button with useFormStatus:
function AddButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? 'Adding...' : 'Add Todo'}</button>;
}
// Main component:
function TodoApp({ initialTodos }) {
const [optimistic, addOptimistic] = useOptimistic(
initialTodos,
(state, newTodo) => [...state, newTodo]
);
const [todos, formAction] = useActionState(
addTodoAction,
initialTodos
);
async function handleAddTodo(formData) {
const text = formData.get('text');
// Optimistic: show it immediately
addOptimistic({ id: Date.now(), text });
// Then submit the form (which updates when server confirms)
await formAction(formData);
}
return (
<>
<ul>
{optimistic.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
<form action={handleAddTodo}>
<input name="text" placeholder="Add a todo..." required />
<AddButton />
</form>
</>
);
}
```
---
## Migration Strategy
### Phase 1 No changes required
Actions are opt-in. All existing `useReducer + onSubmit` patterns continue to work. No forced migration.
### Phase 2 Identify refactor candidates
After React 19 migration stabilizes, profile for `useReducer + async` patterns:
```bash
grep -rn "useReducer.*case.*'loading\|useReducer.*case.*'success" src/ --include="*.js" --include="*.jsx"
```
Patterns worth refactoring:
- Form submissions with loading/error state
- Async operations triggered by user events
- Current code uses `dispatch({ type: '...' })`
- Simple state shape (object with `loading`, `error`, `data`)
### Phase 3 Refactor to useActionState
```jsx
// Before:
function LoginForm() {
const [state, dispatch] = useReducer(loginReducer, { loading: false, error: null, user: null });
async function handleSubmit(e) {
e.preventDefault();
dispatch({ type: 'loading' });
try {
const user = await login(e.target);
dispatch({ type: 'success', data: user });
} catch (err) {
dispatch({ type: 'error', error: err.message });
}
}
return <form onSubmit={handleSubmit}>...</form>;
}
// After:
async function loginAction(prevState, formData) {
try {
const user = await login(formData);
return { user, error: null };
} catch (err) {
return { user: null, error: err.message };
}
}
function LoginForm() {
const [state, formAction] = useActionState(loginAction, { user: null, error: null });
return <form action={formAction}>...</form>;
}
```
---
## Comparison Table
| Feature | React 18 | React 19 |
|---|---|---|
| Form handling | `onSubmit` + useReducer | `action` + useActionState |
| Loading state | Manual dispatch | Automatic `isPending` |
| Child component pending state | Prop drilling | `useFormStatus` hook |
| Optimistic updates | Manual state dance | `useOptimistic` hook |
| Error handling | Manual in dispatch | Return from action |
| Complexity | More boilerplate | Less boilerplate |

View File

@@ -0,0 +1,335 @@
---
title: React 19 Suspense for Data Fetching Pattern Reference
---
# React 19 Suspense for Data Fetching Pattern Reference
React 19's new Suspense integration for **data fetching** is a preview feature that allows components to suspend (pause rendering) until data is available, without `useEffect + state`.
**Important:** This is a **preview** it requires specific setup and is not yet stable for production, but you should know the pattern for React 19 migration planning.
---
## What Changed in React 19?
React 18 Suspense only supported **code splitting** (lazy components). React 19 extends it to **data fetching** if certain conditions are met:
- **Lib usage** the data fetching library must implement Suspense (e.g., React Query 5+, SWR, Remix loaders)
- **Or your own promise tracking** wrap promises in a way React can track their suspension
- **No more "no hook after suspense"** you can use Suspense directly in components with `use()`
---
## React 18 Suspense (Code Splitting Only)
```jsx
// React 18 Suspense for lazy imports only:
const LazyComponent = React.lazy(() => import('./Component'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<LazyComponent />
</Suspense>
);
}
```
Trying to suspend for data in React 18 required hacks or libraries:
```jsx
// React 18 hack not recommended:
const dataPromise = fetchData();
const resource = {
read: () => {
throw dataPromise; // Throw to suspend
}
};
function Component() {
const data = resource.read(); // Throws promise → Suspense catches it
return <div>{data}</div>;
}
```
---
## React 19 Suspense for Data Fetching (Preview)
React 19 provides **first-class support** for Suspense with promises via the `use()` hook:
```jsx
// React 19 Suspense for data fetching:
function UserProfile({ userId }) {
const user = use(fetchUser(userId)); // Suspends if promise pending
return <div>{user.name}</div>;
}
function App() {
return (
<Suspense fallback={<Spinner />}>
<UserProfile userId={123} />
</Suspense>
);
}
```
**Key differences from React 18:**
- `use()` unwraps the promise, component suspends automatically
- No need for `useEffect + state` trick
- Cleaner code, less boilerplate
---
## Pattern 1: Simple Promise Suspense
```jsx
// Raw promise (not recommended in production):
function DataComponent() {
const data = use(fetch('/api/data').then(r => r.json()));
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
function App() {
return (
<Suspense fallback={<Spinner />}>
<DataComponent />
</Suspense>
);
}
```
**Problem:** Promise is recreated every render. Solution: wrap in `useMemo`.
---
## Pattern 2: Memoized Promise (Better)
```jsx
function DataComponent({ id }) {
// Only create promise once per id:
const dataPromise = useMemo(() =>
fetch(`/api/data/${id}`).then(r => r.json()),
[id]
);
const data = use(dataPromise);
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
function App() {
const [id, setId] = useState(1);
return (
<Suspense fallback={<Spinner />}>
<DataComponent id={id} />
<button onClick={() => setId(id + 1)}>Next</button>
</Suspense>
);
}
```
---
## Pattern 3: Library Integration (React Query)
Modern data libraries support Suspense directly. React Query 5+ example:
```jsx
// React Query 5+ with Suspense:
import { useSuspenseQuery } from '@tanstack/react-query';
function UserProfile({ userId }) {
// useSuspenseQuery throws promise if suspended
const { data: user } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
return <div>{user.name}</div>;
}
function App() {
return (
<Suspense fallback={<Spinner />}>
<UserProfile userId={123} />
</Suspense>
);
}
```
**Advantage:** Library handles caching, retries, and cache invalidation.
---
## Pattern 4: Error Boundary Integration
Combine Suspense with Error Boundary to handle both loading and errors:
```jsx
function UserProfile({ userId }) {
const user = use(fetchUser(userId)); // Suspends while loading
return <div>{user.name}</div>;
}
function App() {
return (
<ErrorBoundary fallback={<ErrorScreen />}>
<Suspense fallback={<Spinner />}>
<UserProfile userId={123} />
</Suspense>
</ErrorBoundary>
);
}
class ErrorBoundary extends React.Component {
state = { error: null };
static getDerivedStateFromError(error) {
return { error };
}
render() {
if (this.state.error) return this.props.fallback;
return this.props.children;
}
}
```
---
## Nested Suspense Boundaries
Use multiple Suspense boundaries to show partial UI while waiting for different data:
```jsx
function App({ userId }) {
return (
<div>
<Suspense fallback={<UserSpinner />}>
<UserProfile userId={userId} />
</Suspense>
<Suspense fallback={<PostsSpinner />}>
<UserPosts userId={userId} />
</Suspense>
</div>
);
}
function UserProfile({ userId }) {
const user = use(fetchUser(userId));
return <h1>{user.name}</h1>;
}
function UserPosts({ userId }) {
const posts = use(fetchUserPosts(userId));
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
```
Now:
- User profile shows spinner while loading
- Posts show spinner independently
- Both can render as they complete
---
## Sequential vs Parallel Suspense
### Sequential (wait for first before fetching second)
```jsx
function App({ userId }) {
const user = use(fetchUser(userId)); // Must complete first
return (
<Suspense fallback={<PostsSpinner />}>
<UserPosts userId={user.id} /> {/* Depends on user */}
</Suspense>
);
}
function UserPosts({ userId }) {
const posts = use(fetchUserPosts(userId));
return <ul>{posts.map(p => <li>{p.title}</li>)}</ul>;
}
```
### Parallel (fetch both at once)
```jsx
function App({ userId }) {
return (
<div>
<Suspense fallback={<UserSpinner />}>
<UserProfile userId={userId} />
</Suspense>
<Suspense fallback={<PostsSpinner />}>
<UserPosts userId={userId} /> {/* Fetches in parallel */}
</Suspense>
</div>
);
}
```
---
## Migration Strategy for React 18 → React 19
### Phase 1 No changes required
Suspense is still optional and experimental for data fetching. All existing `useEffect + state` patterns continue to work.
### Phase 2 Wait for stability
Before adopting Suspense data fetching in production:
- Wait for React 19 to ship (not preview)
- Verify your data library supports Suspense
- Plan migration after app stabilizes on React 19 core
### Phase 3 Refactor to Suspense (optional, post-preview)
Once stable, profile candidates:
```bash
grep -rn "useEffect.*fetch\|useEffect.*axios\|useEffect.*graphql" src/ --include="*.js" --include="*.jsx"
```
```jsx
// Before (React 18):
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
if (!user) return <Spinner />;
return <div>{user.name}</div>;
}
// After (React 19 with Suspense):
function UserProfile({ userId }) {
const user = use(fetchUser(userId));
return <div>{user.name}</div>;
}
// Must be wrapped in Suspense:
<Suspense fallback={<Spinner />}>
<UserProfile userId={123} />
</Suspense>
```
---
## Important Warnings
1. **Still Preview** Suspense for data is marked experimental, behavior may change
2. **Performance** promises are recreated on every render without memoization; use `useMemo`
3. **Cache** `use()` doesn't cache; use React Query or similar for production apps
4. **SSR** Suspense SSR support is limited; check Next.js version requirements

View File

@@ -0,0 +1,208 @@
---
title: React 19 use() Hook Pattern Reference
---
# React 19 use() Hook Pattern Reference
The `use()` hook is React 19's answer for unwrapping promises and context within React components. It enables cleaner async patterns directly in your component body, avoiding the architectural complexity that previously required separate lazy components or complex state management.
## What is use()?
`use()` is a hook that:
- **Accepts** a promise or context object
- **Returns** the resolved value or context value
- **Handles** Suspense automatically for promises
- **Can be called conditionally** inside components (not at top level for promises)
- **Throws errors**, which Suspense + error boundary can catch
## use() with Promises
### React 18 Pattern
```jsx
// React 18 approach 1 lazy load a component module:
const UserComponent = React.lazy(() => import('./User'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<UserComponent />
</Suspense>
);
}
// React 18 approach 2 fetch data with state + useEffect:
function App({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
if (!user) return <Spinner />;
return <User user={user} />;
}
```
### React 19 use() Pattern
```jsx
// React 19 use() directly in component:
function App({ userId }) {
const user = use(fetchUser(userId)); // Suspends automatically
return <User user={user} />;
}
// Usage:
function Root() {
return (
<Suspense fallback={<Spinner />}>
<App userId={123} />
</Suspense>
);
}
```
**Key differences:**
- `use()` unwraps the promise directly in the component body
- Suspense boundary is still needed, but can be placed at app root (not per-component)
- No state or useEffect needed for simple async data
- Conditional wrapping allowed inside components
## use() with Promises -- Conditional Fetching
```jsx
// React 18 conditional with state
function SearchResults() {
const [results, setResults] = useState(null);
const [query, setQuery] = useState('');
useEffect(() => {
if (query) {
search(query).then(setResults);
} else {
setResults(null);
}
}, [query]);
if (!results) return null;
return <Results items={results} />;
}
// React 19 use() with conditional
function SearchResults() {
const [query, setQuery] = useState('');
if (!query) return null;
const results = use(search(query)); // Only fetches if query is truthy
return <Results items={results} />;
}
```
## use() with Context
`use()` can unwrap context without being at component root. Less common than promise usage, but useful for conditional context reading:
```jsx
// React 18 always in component body, only works at top level
const theme = useContext(ThemeContext);
// React 19 can be conditional
function Button({ useSystemTheme }) {
const theme = useSystemTheme ? use(ThemeContext) : defaultTheme;
return <button style={theme}>Click</button>;
}
```
---
## Migration Strategy
### Phase 1 No changes required
React 19 `use()` is opt-in. All existing Suspense + component splitting patterns continue to work:
```jsx
// Keep this as-is if it's working:
const Lazy = React.lazy(() => import('./Component'));
<Suspense fallback={<Spinner />}><Lazy /></Suspense>
```
### Phase 2 Post-migration cleanup (optional)
After React 19 migration stabilizes, profile codebases for `useEffect + state` async patterns. These are good candidates for `use()` refactoring:
Identify patterns:
```bash
grep -rn "useEffect.*\(.*fetch\|async\|promise" src/ --include="*.js" --include="*.jsx"
```
Target:
- Simple fetch-on-mount patterns
- No complex dependency arrays
- Single promise per component
- Suspense already in use elsewhere in the app
Example refactor:
```jsx
// Before:
function Post({ postId }) {
const [post, setPost] = useState(null);
useEffect(() => {
fetchPost(postId).then(setPost);
}, [postId]);
if (!post) return <Spinner />;
return <PostContent post={post} />;
}
// After:
function Post({ postId }) {
const post = use(fetchPost(postId));
return <PostContent post={post} />;
}
// And ensure Suspense at app level:
<Suspense fallback={<AppSpinner />}>
<Post postId={123} />
</Suspense>
```
---
## Error Handling
`use()` throws errors, which Suspense error boundaries catch:
```jsx
function Root() {
return (
<ErrorBoundary fallback={<ErrorScreen />}>
<Suspense fallback={<Spinner />}>
<DataComponent />
</Suspense>
</ErrorBoundary>
);
}
function DataComponent() {
const data = use(fetchData()); // If fetch rejects, error boundary catches it
return <Data data={data} />;
}
```
---
## When NOT to use use()
- **Avoid during migration** stabilize React 19 first
- **Complex dependencies** if multiple promises or complex ordering logic, stick with `useEffect`
- **Retry logic** `use()` doesn't handle retry; `useEffect` with state is clearer
- **Debounced updates** `use()` refetches on every prop change; `useEffect` with cleanup is better

View File

@@ -0,0 +1,37 @@
---
name: react19-source-patterns
description: 'Reference for React 19 source-file migration patterns, including API changes, ref handling, and context updates.'
---
# React 19 Source Migration Patterns
Reference for every source-file migration required for React 19.
## Quick Reference Table
| Pattern | Action | Reference |
|---|---|---|
| `ReactDOM.render(...)` | → `createRoot().render()` | See references/api-migrations.md |
| `ReactDOM.hydrate(...)` | → `hydrateRoot(...)` | See references/api-migrations.md |
| `unmountComponentAtNode` | → `root.unmount()` | Inline fix |
| `ReactDOM.findDOMNode` | → direct ref | Inline fix |
| `forwardRef(...)` wrapper | → ref as direct prop | See references/api-migrations.md |
| `Component.defaultProps = {}` | → ES6 default params | See references/api-migrations.md |
| `useRef()` no arg | → `useRef(null)` | Inline fix add `null` |
| Legacy Context | → `createContext` | [→ api-migrations.md#legacy-context](references/api-migrations.md#legacy-context) |
| String refs `this.refs.x` | → `createRef()` | [→ api-migrations.md#string-refs](references/api-migrations.md#string-refs) |
| `import React from 'react'` (unused) | Remove | Only if no `React.` usage in file |
## PropTypes Rule
Do **not** remove `.propTypes` assignments. The `prop-types` package still works as a standalone validator. React 19 only removes the built-in runtime checking from the React package the package itself remains valid.
Add this comment above any `.propTypes` block:
```jsx
// NOTE: React 19 no longer runs propTypes validation at runtime.
// PropTypes kept for documentation and IDE tooling only.
```
## Read the Reference
For full before/after code for each migration, read **`references/api-migrations.md`**. It contains the complete patterns including edge cases for `forwardRef` with `useImperativeHandle`, `defaultProps` null vs undefined behavior, and legacy context provider/consumer cross-file migrations.

View File

@@ -0,0 +1,515 @@
---
title: React 19 API Migrations Reference
---
# React 19 API Migrations Reference
Complete before/after patterns for all React 19 breaking changes and removed APIs.
---
## ReactDOM Root API Migration
React 19 requires `createRoot()` or `hydrateRoot()` for all apps. If the React 18 migration already ran, this is done. Verify it's correct.
### Pattern 1: createRoot() CSR App
```jsx
// Before (React 18 or earlier):
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));
// After (React 19):
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
```
### Pattern 2: hydrateRoot() SSR/Static App
```jsx
// Before (React 18 server-rendered app):
import ReactDOM from 'react-dom';
ReactDOM.hydrate(<App />, document.getElementById('root'));
// After (React 19):
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);
```
### Pattern 3: unmountComponentAtNode() Removed
```jsx
// Before (React 18):
import ReactDOM from 'react-dom';
ReactDOM.unmountComponentAtNode(container);
// After (React 19):
const root = createRoot(container); // Save the root reference
// later:
root.unmount();
```
**Caveat:** If the root reference was never saved, you must refactor to pass it around or use a global registry.
---
## findDOMNode() Removed
### Pattern 1: Direct ref
```jsx
// Before (React 18):
import { findDOMNode } from 'react-dom';
const domNode = findDOMNode(componentRef);
// After (React 19):
const domNode = componentRef.current; // refs point directly to DOM
```
### Pattern 2: Class Component ref
```jsx
// Before (React 18):
import { findDOMNode } from 'react-dom';
class MyComponent extends React.Component {
render() {
return <div ref={ref => this.node = ref}>Content</div>;
}
getWidth() {
return findDOMNode(this).offsetWidth;
}
}
// After (React 19):
// Note: findDOMNode() is removed in React 19. Eliminate the call entirely
// and use direct refs to access DOM nodes instead.
class MyComponent extends React.Component {
nodeRef = React.createRef();
render() {
return <div ref={this.nodeRef}>Content</div>;
}
getWidth() {
return this.nodeRef.current.offsetWidth;
}
}
```
---
## forwardRef() - Optional Modernization
### Pattern 1: Function Component Direct ref
```jsx
// Before (React 18):
import { forwardRef } from 'react';
const Input = forwardRef((props, ref) => (
<input ref={ref} {...props} />
));
function App() {
const inputRef = useRef(null);
return <Input ref={inputRef} />;
}
// After (React 19):
// Simply accept ref as a regular prop:
function Input({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
function App() {
const inputRef = useRef(null);
return <Input ref={inputRef} />;
}
```
### Pattern 2: forwardRef + useImperativeHandle
```jsx
// Before (React 18):
import { forwardRef, useImperativeHandle } from 'react';
const TextInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
clear: () => { inputRef.current.value = ''; }
}));
return <input ref={inputRef} {...props} />;
});
function App() {
const textRef = useRef(null);
return (
<>
<TextInput ref={textRef} />
<button onClick={() => textRef.current.focus()}>Focus</button>
</>
);
}
// After (React 19):
function TextInput({ ref, ...props }) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
clear: () => { inputRef.current.value = ''; }
}));
return <input ref={inputRef} {...props} />;
}
function App() {
const textRef = useRef(null);
return (
<>
<TextInput ref={textRef} />
<button onClick={() => textRef.current.focus()}>Focus</button>
</>
);
}
```
**Note:** `useImperativeHandle` is still valid; only the `forwardRef` wrapper is removed.
---
## defaultProps Removed
### Pattern 1: Function Component with defaultProps
```jsx
// Before (React 18):
function Button({ label = 'Click', disabled = false }) {
return <button disabled={disabled}>{label}</button>;
}
// WORKS BUT is removed in React 19:
Button.defaultProps = {
label: 'Click',
disabled: false
};
// After (React 19):
// ES6 default params are now the ONLY way:
function Button({ label = 'Click', disabled = false }) {
return <button disabled={disabled}>{label}</button>;
}
// Remove all defaultProps assignments
```
### Pattern 2: Class Component defaultProps
```jsx
// Before (React 18):
class Button extends React.Component {
static defaultProps = {
label: 'Click',
disabled: false
};
render() {
return <button disabled={this.props.disabled}>{this.props.label}</button>;
}
}
// After (React 19):
// Use default params in constructor or class field:
class Button extends React.Component {
constructor(props) {
super(props);
this.label = props.label || 'Click';
this.disabled = props.disabled || false;
}
render() {
return <button disabled={this.disabled}>{this.label}</button>;
}
}
// Or simplify to function component with ES6 defaults:
function Button({ label = 'Click', disabled = false }) {
return <button disabled={disabled}>{label}</button>;
}
```
### Pattern 3: defaultProps with null
```jsx
// Before (React 18):
function Component({ value }) {
// defaultProps can set null to reset a parent-passed value
return <div>{value}</div>;
}
Component.defaultProps = {
value: null
};
// After (React 19):
// Use explicit null checks or nullish coalescing:
function Component({ value = null }) {
return <div>{value}</div>;
}
// Or:
function Component({ value }) {
return <div>{value ?? null}</div>;
}
```
---
## useRef Without Initial Value
### Pattern 1: useRef()
```jsx
// Before (React 18):
const ref = useRef(); // undefined initially
// After (React 19):
// Explicitly pass null as initial value:
const ref = useRef(null);
// Then use current:
ref.current = someElement; // Set it manually later
```
### Pattern 2: useRef with DOM Elements
```jsx
// Before:
function Component() {
const inputRef = useRef();
return <input ref={inputRef} />;
}
// After:
function Component() {
const inputRef = useRef(null); // Explicit null
return <input ref={inputRef} />;
}
```
---
## Legacy Context API Removed
### Pattern 1: React.createContext vs contextTypes
```jsx
// Before (React 18 not recommended but worked):
// Using contextTypes (old PropTypes-style context):
class MyComponent extends React.Component {
static contextTypes = {
theme: PropTypes.string
};
render() {
return <div style={{ color: this.context.theme }}>Text</div>;
}
}
// Provider using getChildContext (old API):
class App extends React.Component {
static childContextTypes = {
theme: PropTypes.string
};
getChildContext() {
return { theme: 'dark' };
}
render() {
return <MyComponent />;
}
}
// After (React 19):
// Use createContext (modern API):
const ThemeContext = React.createContext(null);
function MyComponent() {
const theme = useContext(ThemeContext);
return <div style={{ color: theme }}>Text</div>;
}
function App() {
return (
<ThemeContext.Provider value="dark">
<MyComponent />
</ThemeContext.Provider>
);
}
```
### Pattern 2: Class Component Consuming createContext
```jsx
// Before (class component consuming old context):
class MyComponent extends React.Component {
static contextType = ThemeContext;
render() {
return <div style={{ color: this.context }}>Text</div>;
}
}
// After (still works in React 19):
// No change needed for static contextType
// Continue using this.context
```
**Important:** If you're still using the old `contextTypes` + `getChildContext` pattern (not modern `createContext`), you **must** migrate to `createContext` the old pattern is completely removed.
---
## String Refs Removed
### Pattern 1: this.refs String Refs
```jsx
// Before (React 18):
class Component extends React.Component {
render() {
return (
<>
<input ref="inputRef" />
<button onClick={() => this.refs.inputRef.focus()}>Focus</button>
</>
);
}
}
// After (React 19):
class Component extends React.Component {
inputRef = React.createRef();
render() {
return (
<>
<input ref={this.inputRef} />
<button onClick={() => this.inputRef.current.focus()}>Focus</button>
</>
);
}
}
```
### Pattern 2: Callback Refs (Recommended)
```jsx
// Before (React 18):
class Component extends React.Component {
render() {
return (
<>
<input ref="inputRef" />
<button onClick={() => this.refs.inputRef.focus()}>Focus</button>
</>
);
}
}
// After (React 19 callback is more flexible):
class Component extends React.Component {
constructor(props) {
super(props);
this.inputRef = null;
}
render() {
return (
<>
<input ref={(el) => { this.inputRef = el; }} />
<button onClick={() => this.inputRef?.focus()}>Focus</button>
</>
);
}
}
```
---
## Unused React Import Removal
### Pattern 1: React Import After JSX Transform
```jsx
// Before (React 18):
import React from 'react'; // Needed for JSX transform
function Component() {
return <div>Text</div>;
}
// After (React 19 with new JSX transform):
// Remove the React import if it's not used:
function Component() {
return <div>Text</div>;
}
// BUT keep it if you use React.* APIs:
import React from 'react';
function Component() {
return <div>{React.useState ? 'yes' : 'no'}</div>;
}
```
### Scan for Unused React Imports
```bash
# Find imports that can be removed:
grep -rn "^import React from 'react';" src/ --include="*.js" --include="*.jsx"
# Then check if the file uses React.*, useContext, etc.
```
---
## Complete Migration Checklist
```bash
# 1. Find all ReactDOM.render calls:
grep -rn "ReactDOM.render" src/ --include="*.js" --include="*.jsx"
# Should be converted to createRoot
# 2. Find all ReactDOM.hydrate calls:
grep -rn "ReactDOM.hydrate" src/ --include="*.js" --include="*.jsx"
# Should be converted to hydrateRoot
# 3. Find all forwardRef usages:
grep -rn "forwardRef" src/ --include="*.js" --include="*.jsx"
# Check each one to see if it can be removed (most can)
# 4. Find all .defaultProps assignments:
grep -rn "\.defaultProps\s*=" src/ --include="*.js" --include="*.jsx"
# Replace with ES6 default params
# 5. Find all useRef() without initial value:
grep -rn "useRef()" src/ --include="*.js" --include="*.jsx"
# Add null: useRef(null)
# 6. Find old context (contextTypes):
grep -rn "contextTypes\|childContextTypes\|getChildContext" src/ --include="*.js" --include="*.jsx"
# Migrate to createContext
# 7. Find string refs (ref="name"):
grep -rn 'ref="' src/ --include="*.js" --include="*.jsx"
# Migrate to createRef or callback ref
# 8. Find unused React imports:
grep -rn "^import React from 'react';" src/ --include="*.js" --include="*.jsx"
# Check if React is used in the file
```

View File

@@ -0,0 +1,100 @@
---
name: react19-test-patterns
description: 'Provides before/after patterns for migrating test files to React 19 compatibility, including act() imports, Simulate removal, and StrictMode call count changes.'
---
# React 19 Test Migration Patterns
Reference for all test file migrations required by React 19.
## Priority Order
Fix test files in this order; each layer depends on the previous:
1. **`act` import** fix first, it unblocks everything else
2. **`Simulate``fireEvent`** fix immediately after act
3. **Full react-dom/test-utils cleanup** remove remaining imports
4. **StrictMode call counts** measure actual, don't guess
5. **Async act wrapping** for remaining "not wrapped in act" warnings
6. **Custom render helper** verify once per codebase, not per test
---
## 1. act() Import Fix
```jsx
// Before REMOVED in React 19:
import { act } from 'react-dom/test-utils';
// After:
import { act } from 'react';
```
If mixed with other test-utils imports:
```jsx
// Before:
import { act, Simulate, renderIntoDocument } from 'react-dom/test-utils';
// After split the imports:
import { act } from 'react';
import { fireEvent, render } from '@testing-library/react'; // replaces Simulate + renderIntoDocument
```
---
## 2. Simulate → fireEvent
```jsx
// Before Simulate REMOVED in React 19:
import { Simulate } from 'react-dom/test-utils';
Simulate.click(element);
Simulate.change(input, { target: { value: 'hello' } });
Simulate.submit(form);
Simulate.keyDown(element, { key: 'Enter', keyCode: 13 });
// After:
import { fireEvent } from '@testing-library/react';
fireEvent.click(element);
fireEvent.change(input, { target: { value: 'hello' } });
fireEvent.submit(form);
fireEvent.keyDown(element, { key: 'Enter', keyCode: 13 });
```
---
## 3. react-dom/test-utils Full API Map
| Old (react-dom/test-utils) | New location |
|---|---|
| `act` | `import { act } from 'react'` |
| `Simulate` | `fireEvent` from `@testing-library/react` |
| `renderIntoDocument` | `render` from `@testing-library/react` |
| `findRenderedDOMComponentWithTag` | `getByRole`, `getByTestId` from RTL |
| `findRenderedDOMComponentWithClass` | `getByRole` or `container.querySelector` |
| `scryRenderedDOMComponentsWithTag` | `getAllByRole` from RTL |
| `isElement`, `isCompositeComponent` | Remove not needed with RTL |
| `isDOMComponent` | Remove |
---
## 4. StrictMode Call Count Fixes
React 19 StrictMode no longer double-invokes `useEffect` in development. Spy assertions counting effect calls must be updated.
**Strategy always measure, never guess:**
```bash
# Run the failing test, read the actual count from the error:
npm test -- --watchAll=false --testPathPattern="[filename]" --forceExit 2>&1 | grep -E "Expected|Received"
```
```jsx
// Before (React 18 StrictMode effects ran twice):
expect(mockFn).toHaveBeenCalledTimes(2); // 1 call × 2 (strict double-invoke)
// After (React 19 StrictMode effects run once):
expect(mockFn).toHaveBeenCalledTimes(1);
```
```jsx
// Render-phase calls (component body) still double-invoked in React 19 StrictMode:
expect(renderSpy).toHaveBeenCalledTimes(2); // stays at 2 for render body calls