mirror of
https://github.com/github/awesome-copilot.git
synced 2026-04-11 02:35:55 +00:00
chore: publish from staged
This commit is contained in:
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
227
plugins/react19-upgrade/agents/react19-auditor.md
Normal file
227
plugins/react19-upgrade/agents/react19-auditor.md
Normal 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.
|
||||
224
plugins/react19-upgrade/agents/react19-commander.md
Normal file
224
plugins/react19-upgrade/agents/react19-commander.md
Normal 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
|
||||
139
plugins/react19-upgrade/agents/react19-dep-surgeon.md
Normal file
139
plugins/react19-upgrade/agents/react19-dep-surgeon.md
Normal 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.
|
||||
226
plugins/react19-upgrade/agents/react19-migrator.md
Normal file
226
plugins/react19-upgrade/agents/react19-migrator.md
Normal 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.
|
||||
245
plugins/react19-upgrade/agents/react19-test-guardian.md
Normal file
245
plugins/react19-upgrade/agents/react19-test-guardian.md
Normal 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 (T1–T8) 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.
|
||||
@@ -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.
|
||||
@@ -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 |
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
100
plugins/react19-upgrade/skills/react19-test-patterns/SKILL.md
Normal file
100
plugins/react19-upgrade/skills/react19-test-patterns/SKILL.md
Normal 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
|
||||
Reference in New Issue
Block a user