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

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

View File

@@ -0,0 +1,36 @@
---
name: react-audit-grep-patterns
description: 'Provides the complete, verified grep scan command library for auditing React codebases before a React 18.3.1 or React 19 upgrade. Use this skill whenever running a migration audit - for both the react18-auditor and react19-auditor agents. Contains every grep pattern needed to find deprecated APIs, removed APIs, unsafe lifecycle methods, batching vulnerabilities, test file issues, dependency conflicts, and React 19 specific removals. Always use this skill when writing audit scan commands - do not rely on memory for grep syntax, especially for the multi-line async setState patterns which require context flags.'
---
# React Audit Grep Patterns
Complete scan command library for React 18.3.1 and React 19 migration audits.
## Usage
Read the relevant section for your target:
- **`references/react18-scans.md`** - all scans for React 16/17 → 18.3.1 audit
- **`references/react19-scans.md`** - all scans for React 18 → 19 audit
- **`references/test-scans.md`** - test file specific scans (used by both auditors)
- **`references/dep-scans.md`** - dependency and peer conflict scans
## Base Patterns Used Across All Scans
```bash
# Standard flags used throughout:
# -r = recursive
# -n = show line numbers
# -l = show filenames only (for counting affected files)
# --include="*.js" --include="*.jsx" = JS/JSX files only
# | grep -v "\.test\.\|\.spec\.\|__tests__" = exclude test files
# | grep -v "node_modules" = safety (usually handled by not scanning node_modules)
# 2>/dev/null = suppress "no files found" errors
# Source files only (exclude tests):
SRC_FLAGS='--include="*.js" --include="*.jsx"'
EXCLUDE_TESTS='grep -v "\.test\.\|\.spec\.\|__tests__"'
# Test files only:
TEST_FLAGS='--include="*.test.js" --include="*.test.jsx" --include="*.spec.js" --include="*.spec.jsx"'
```

View File

@@ -0,0 +1,94 @@
# Dependency Scans - Both Auditors
Scans for dependency compatibility and peer conflicts. Run during both R18 and R19 audits.
---
## Current Versions
```bash
# All react-related package versions in one shot
cat package.json | python3 -c "
import sys, json
d = json.load(sys.stdin)
deps = {**d.get('dependencies',{}), **d.get('devDependencies',{})}
keys = ['react', 'react-dom', 'react-router', 'react-router-dom',
'@testing-library/react', '@testing-library/jest-dom',
'@testing-library/user-event', '@apollo/client', 'graphql',
'@emotion/react', '@emotion/styled', 'jest', 'enzyme',
'react-redux', '@reduxjs/toolkit', 'prop-types']
for k in keys:
if k in deps:
print(f'{k}: {deps[k]}')
" 2>/dev/null
```
---
## Peer Dependency Conflicts
```bash
# All peer dep warnings (must be 0 before migration completes)
npm ls 2>&1 | grep -E "WARN|ERR|peer|invalid|unmet"
# Count of peer errors
npm ls 2>&1 | grep -E "WARN|ERR|peer|invalid|unmet" | wc -l
# Specific package peer dep requirements
npm info @testing-library/react peerDependencies 2>/dev/null
npm info @apollo/client peerDependencies 2>/dev/null
npm info @emotion/react peerDependencies 2>/dev/null
npm info react-router-dom peerDependencies 2>/dev/null
```
---
## Enzyme Detection (R18 Blocker)
```bash
# In package.json
cat package.json | python3 -c "
import sys, json
d = json.load(sys.stdin)
deps = {**d.get('dependencies',{}), **d.get('devDependencies',{})}
enzyme = {k: v for k, v in deps.items() if 'enzyme' in k.lower()}
if enzyme:
print('BLOCKER - Enzyme found:', enzyme)
else:
print('No Enzyme - OK')
" 2>/dev/null
# Enzyme adapter files
find . -name "enzyme-adapter*" -not -path "*/node_modules/*" 2>/dev/null
```
---
## React Router Version Check
```bash
ROUTER=$(node -e "console.log(require('./node_modules/react-router-dom/package.json').version)" 2>/dev/null)
echo "react-router-dom version: $ROUTER"
# If v5 - flag for assessment
if [[ $ROUTER == 5* ]]; then
echo "WARNING: react-router v5 found - requires scope assessment before upgrade"
echo "Run router migration scope scan:"
echo " Routes: $(grep -rn "<Route\|<Switch\|<Redirect" src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." | wc -l) hits"
echo " useHistory: $(grep -rn "useHistory()" src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." | wc -l) hits"
fi
```
---
## Lock File Consistency
```bash
# Check lockfile is in sync with package.json
npm ls --depth=0 2>&1 | head -20
# Check for duplicate react installs (can cause hooks errors)
find node_modules -name "package.json" -path "*/react/package.json" 2>/dev/null \
| grep -v "node_modules/node_modules" \
| xargs grep '"version"' | sort -u
```

View File

@@ -0,0 +1,231 @@
# React 18.3.1 Audit - Complete Scan Commands
Run in this order. Each section maps to a phase in the react18-auditor.
---
## Phase 0 - Codebase Profile
```bash
# Total source files (excluding tests)
find src/ \( -name "*.js" -o -name "*.jsx" \) \
| grep -v "\.test\.\|\.spec\.\|__tests__\|node_modules" \
| wc -l
# Class component count
grep -rl "extends React\.Component\|extends Component\|extends PureComponent" \
src/ --include="*.js" --include="*.jsx" \
| grep -v "\.test\." | wc -l
# Function component rough count
grep -rl "const [A-Z][a-zA-Z]* = \|function [A-Z][a-zA-Z]*(" \
src/ --include="*.js" --include="*.jsx" \
| grep -v "\.test\." | wc -l
# Current React version
node -e "console.log(require('./node_modules/react/package.json').version)" 2>/dev/null
# StrictMode in use? (affects how many lifecycle warnings were already seen)
grep -rn "StrictMode\|React\.StrictMode" \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\."
```
---
## Phase 1 - Unsafe Lifecycle Methods
```bash
# componentWillMount (without UNSAFE_ prefix)
grep -rn "componentWillMount\b" \
src/ --include="*.js" --include="*.jsx" \
| grep -v "UNSAFE_componentWillMount\|\.test\."
# componentWillReceiveProps (without UNSAFE_ prefix)
grep -rn "componentWillReceiveProps\b" \
src/ --include="*.js" --include="*.jsx" \
| grep -v "UNSAFE_componentWillReceiveProps\|\.test\."
# componentWillUpdate (without UNSAFE_ prefix)
grep -rn "componentWillUpdate\b" \
src/ --include="*.js" --include="*.jsx" \
| grep -v "UNSAFE_componentWillUpdate\|\.test\."
# Already partially migrated with UNSAFE_ prefix? (check if team already did partial work)
grep -rn "UNSAFE_component" \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\."
# Quick count summary:
echo "=== Lifecycle Issue Summary ==="
echo "componentWillMount: $(grep -rn "componentWillMount\b" src/ --include="*.js" --include="*.jsx" | grep -v "UNSAFE_\|\.test\." | wc -l)"
echo "componentWillReceiveProps: $(grep -rn "componentWillReceiveProps\b" src/ --include="*.js" --include="*.jsx" | grep -v "UNSAFE_\|\.test\." | wc -l)"
echo "componentWillUpdate: $(grep -rn "componentWillUpdate\b" src/ --include="*.js" --include="*.jsx" | grep -v "UNSAFE_\|\.test\." | wc -l)"
```
---
## Phase 2 - Automatic Batching Vulnerabilities
```bash
# Async class methods (primary risk zone)
grep -rn "^\s*async [a-zA-Z]" \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\."
# Arrow function async methods
grep -rn "=\s*async\s*(" \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\."
# setState inside .then() callbacks
grep -rn "\.then\s*(" \
src/ --include="*.js" --include="*.jsx" -A 3 \
| grep "setState" | grep -v "\.test\."
# setState inside .catch() callbacks
grep -rn "\.catch\s*(" \
src/ --include="*.js" --include="*.jsx" -A 3 \
| grep "setState" | grep -v "\.test\."
# setState inside setTimeout
grep -rn "setTimeout" \
src/ --include="*.js" --include="*.jsx" -A 5 \
| grep "setState" | grep -v "\.test\."
# this.state reads that follow an await (most dangerous pattern)
grep -rn "this\.state\." \
src/ --include="*.js" --include="*.jsx" -B 3 \
| grep "await" | grep -v "\.test\."
# document/window event handlers with setState
grep -rn "addEventListener" \
src/ --include="*.js" --include="*.jsx" -A 5 \
| grep "setState" | grep -v "\.test\."
```
---
## Phase 3 - Legacy Context API
```bash
# Provider side
grep -rn "childContextTypes\s*=" \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\."
grep -rn "getChildContext\s*(" \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\."
# Consumer side
grep -rn "contextTypes\s*=" \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\."
# this.context usage (may indicate legacy or modern - verify per hit)
grep -rn "this\.context\." \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\."
# Count of distinct legacy contexts (by counting childContextTypes blocks)
grep -rn "childContextTypes" \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." | wc -l
```
---
## Phase 4 - String Refs
```bash
# String ref assignments in JSX
grep -rn 'ref="[^"]*"' \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\."
# Alternative quote style
grep -rn "ref='[^']*'" \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\."
# this.refs accessor usage
grep -rn "this\.refs\." \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\."
```
---
## Phase 5 - findDOMNode
```bash
grep -rn "findDOMNode\|ReactDOM\.findDOMNode" \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\."
```
---
## Phase 6 - Root API (ReactDOM.render)
```bash
grep -rn "ReactDOM\.render\s*(" \
src/ --include="*.js" --include="*.jsx"
grep -rn "ReactDOM\.hydrate\s*(" \
src/ --include="*.js" --include="*.jsx"
grep -rn "unmountComponentAtNode" \
src/ --include="*.js" --include="*.jsx"
```
---
## Phase 7 - Event Delegation (React 16 Carry-Over)
```bash
# document-level event listeners (may miss React events after React 17 delegation change)
grep -rn "document\.addEventListener\|document\.removeEventListener" \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\."
# window event listeners
grep -rn "window\.addEventListener" \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\."
```
---
## Phase 8 - Enzyme Detection (Hard Blocker)
```bash
# Enzyme in package.json
cat package.json | python3 -c "
import sys, json
d = json.load(sys.stdin)
deps = {**d.get('dependencies',{}), **d.get('devDependencies',{})}
enzyme_pkgs = [k for k in deps if 'enzyme' in k.lower()]
print('Enzyme packages found:', enzyme_pkgs if enzyme_pkgs else 'NONE')
"
# Enzyme imports in test files
grep -rn "from 'enzyme'\|require.*enzyme" \
src/ --include="*.test.*" --include="*.spec.*" 2>/dev/null | wc -l
```
---
## Full Summary Script
Run this for a quick overview before detailed scanning:
```bash
#!/bin/bash
echo "=============================="
echo "React 18 Migration Audit Summary"
echo "=============================="
echo ""
echo "LIFECYCLE METHODS:"
echo " componentWillMount: $(grep -rn "componentWillMount\b" src/ --include="*.js" --include="*.jsx" | grep -v "UNSAFE_\|\.test\." | wc -l | tr -d ' ') hits"
echo " componentWillReceiveProps: $(grep -rn "componentWillReceiveProps\b" src/ --include="*.js" --include="*.jsx" | grep -v "UNSAFE_\|\.test\." | wc -l | tr -d ' ') hits"
echo " componentWillUpdate: $(grep -rn "componentWillUpdate\b" src/ --include="*.js" --include="*.jsx" | grep -v "UNSAFE_\|\.test\." | wc -l | tr -d ' ') hits"
echo ""
echo "LEGACY APIS:"
echo " Legacy context (providers): $(grep -rn "childContextTypes" src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." | wc -l | tr -d ' ') hits"
echo " String refs (this.refs): $(grep -rn "this\.refs\." src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." | wc -l | tr -d ' ') hits"
echo " findDOMNode: $(grep -rn "findDOMNode" src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." | wc -l | tr -d ' ') hits"
echo " ReactDOM.render: $(grep -rn "ReactDOM\.render\s*(" src/ --include="*.js" --include="*.jsx" | wc -l | tr -d ' ') hits"
echo ""
echo "ENZYME (BLOCKER):"
echo " Enzyme test files: $(grep -rl "from 'enzyme'" src/ --include="*.test.*" 2>/dev/null | wc -l | tr -d ' ') files"
echo ""
echo "ASYNC BATCHING RISK:"
echo " Async class methods: $(grep -rn "^\s*async [a-zA-Z]" src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." | wc -l | tr -d ' ') hits"
```

View File

@@ -0,0 +1,141 @@
# React 19 Audit - Complete Scan Commands
Run in this order. Each section maps to a phase in the react19-auditor.
---
## Phase 1 - Removed APIs (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\|ReactDOM\.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 imports - most exports REMOVED
grep -rn "from 'react-dom/test-utils'\|from \"react-dom/test-utils\"\|require.*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" | grep -v "\.test\." 2>/dev/null
# 8. String refs - REMOVED
grep -rn "this\.refs\." \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." 2>/dev/null
```
---
## Phase 2 - Deprecated APIs (Should Migrate)
```bash
# 9. forwardRef - deprecated (ref now direct prop)
grep -rn "forwardRef\|React\.forwardRef" \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." 2>/dev/null
# 10. defaultProps on function components - REMOVED for function components
grep -rn "\.defaultProps\s*=" \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." 2>/dev/null
# 11. useRef() without initial value
grep -rn "useRef()\|useRef( )" \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." 2>/dev/null
# 12. propTypes (runtime validation silently dropped)
grep -rn "\.propTypes\s*=" \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." | wc -l
# 13. react-test-renderer - deprecated
grep -rn "react-test-renderer\|TestRenderer" \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." 2>/dev/null
# 14. Unnecessary React default imports (new JSX transform)
grep -rn "^import React from 'react'" \
src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." 2>/dev/null
```
---
## Phase 3 - Test File Scans
```bash
# act() from wrong location
grep -rn "from 'react-dom/test-utils'" \
src/ --include="*.test.js" --include="*.test.jsx" \
--include="*.spec.js" --include="*.spec.jsx" 2>/dev/null
# Simulate usage - REMOVED
grep -rn "Simulate\." \
src/ --include="*.test.*" --include="*.spec.*" 2>/dev/null
# react-test-renderer in tests
grep -rn "from 'react-test-renderer'" \
src/ --include="*.test.*" --include="*.spec.*" 2>/dev/null
# Spy call count assertions (may need StrictMode delta updates)
grep -rn "toHaveBeenCalledTimes" \
src/ --include="*.test.*" --include="*.spec.*" | head -20 2>/dev/null
# console.error call count assertions (React 19 error reporting change)
grep -rn "console\.error.*toHaveBeenCalledTimes\|toHaveBeenCalledTimes.*console\.error" \
src/ --include="*.test.*" --include="*.spec.*" 2>/dev/null
```
---
## Phase 4 - StrictMode Behavioral Changes
```bash
# StrictMode usage
grep -rn "StrictMode\|React\.StrictMode" \
src/ --include="*.js" --include="*.jsx" 2>/dev/null
# Spy assertions that may be affected by StrictMode double-invoke change
grep -rn "toHaveBeenCalledTimes\|\.mock\.calls\.length" \
src/ --include="*.test.*" --include="*.spec.*" 2>/dev/null
```
---
## Full Summary Script
```bash
#!/bin/bash
echo "=============================="
echo "React 19 Migration Audit Summary"
echo "=============================="
echo ""
echo "REMOVED APIs (Critical):"
echo " ReactDOM.render: $(grep -rn "ReactDOM\.render\s*(" src/ --include="*.js" --include="*.jsx" | wc -l | tr -d ' ') hits"
echo " ReactDOM.hydrate: $(grep -rn "ReactDOM\.hydrate\s*(" src/ --include="*.js" --include="*.jsx" | wc -l | tr -d ' ') hits"
echo " unmountComponentAtNode: $(grep -rn "unmountComponentAtNode" src/ --include="*.js" --include="*.jsx" | wc -l | tr -d ' ') hits"
echo " findDOMNode: $(grep -rn "findDOMNode" src/ --include="*.js" --include="*.jsx" | wc -l | tr -d ' ') hits"
echo " react-dom/test-utils: $(grep -rn "from 'react-dom/test-utils'" src/ --include="*.js" --include="*.jsx" | wc -l | tr -d ' ') hits"
echo " Legacy context: $(grep -rn "contextTypes\|childContextTypes\|getChildContext" src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." | wc -l | tr -d ' ') hits"
echo " String refs: $(grep -rn "this\.refs\." src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." | wc -l | tr -d ' ') hits"
echo ""
echo "DEPRECATED APIs:"
echo " forwardRef: $(grep -rn "forwardRef" src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." | wc -l | tr -d ' ') hits"
echo " defaultProps (fn comps): $(grep -rn "\.defaultProps\s*=" src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." | wc -l | tr -d ' ') hits"
echo " useRef() no arg: $(grep -rn "useRef()" src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." | wc -l | tr -d ' ') hits"
echo ""
echo "TEST FILE ISSUES:"
echo " react-dom/test-utils: $(grep -rn "from 'react-dom/test-utils'" src/ --include="*.test.*" --include="*.spec.*" | wc -l | tr -d ' ') hits"
echo " Simulate usage: $(grep -rn "Simulate\." src/ --include="*.test.*" | wc -l | tr -d ' ') hits"
```

View File

@@ -0,0 +1,94 @@
# Test File Scans - Both Auditors
Scans specifically for test file issues. Run during both R18 and R19 audits.
---
## Setup Files
```bash
# Find test setup files
find src/ -name "setupTests*" -o -name "jest.setup*" 2>/dev/null
find . -name "jest.config.js" -o -name "jest.config.ts" 2>/dev/null | grep -v "node_modules"
# Check setup file for legacy patterns
grep -n "ReactDOM\|react-dom/test-utils\|Enzyme\|configure\|Adapter" \
src/setupTests.js 2>/dev/null
```
---
## Import Scans
```bash
# All react-dom/test-utils imports in tests
grep -rn "from 'react-dom/test-utils'\|require.*react-dom/test-utils" \
src/ --include="*.test.*" --include="*.spec.*" 2>/dev/null
# Enzyme imports
grep -rn "from 'enzyme'\|require.*enzyme" \
src/ --include="*.test.*" --include="*.spec.*" 2>/dev/null
# react-test-renderer
grep -rn "from 'react-test-renderer'" \
src/ --include="*.test.*" --include="*.spec.*" 2>/dev/null
# Old act location
grep -rn "act.*from 'react-dom'" \
src/ --include="*.test.*" --include="*.spec.*" 2>/dev/null
```
---
## Render Pattern Scans
```bash
# ReactDOM.render in tests (should use RTL render)
grep -rn "ReactDOM\.render\s*(" \
src/ --include="*.test.*" --include="*.spec.*" 2>/dev/null
# Enzyme shallow/mount
grep -rn "shallow(\|mount(" \
src/ --include="*.test.*" --include="*.spec.*" 2>/dev/null
# Custom render helpers
find src/ -name "test-utils.js" -o -name "renderWithProviders*" \
-o -name "customRender*" -o -name "render-helpers*" 2>/dev/null
```
---
## Assertion Scans
```bash
# Call count assertions (StrictMode sensitive)
grep -rn "toHaveBeenCalledTimes" \
src/ --include="*.test.*" --include="*.spec.*" 2>/dev/null
# console.error assertions (React error logging changed in R19)
grep -rn "console\.error" \
src/ --include="*.test.*" --include="*.spec.*" 2>/dev/null
# Intermediate state assertions (batching sensitive)
grep -rn "fireEvent\|userEvent" \
src/ --include="*.test.*" --include="*.spec.*" -A 1 \
| grep "expect\|getBy\|queryBy" | head -20 2>/dev/null
```
---
## Async Scans
```bash
# act() usage
grep -rn "\bact(" \
src/ --include="*.test.*" --include="*.spec.*" 2>/dev/null
# waitFor usage (good - check these are properly async)
grep -rn "waitFor\|findBy" \
src/ --include="*.test.*" --include="*.spec.*" | wc -l
# setTimeout in tests (may be batching-sensitive)
grep -rn "setTimeout\|setInterval" \
src/ --include="*.test.*" --include="*.spec.*" 2>/dev/null
```

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,102 @@
---
name: react18-dep-compatibility
description: 'React 18.3.1 and React 19 dependency compatibility matrix.'
---
# React Dependency Compatibility Matrix
Minimum versions required for React 18.3.1 and React 19 compatibility.
Use this skill whenever checking whether a dependency supports a target React version, resolving peer dependency conflicts, deciding whether to upgrade or use `legacy-peer-deps`, or assessing the risk of a `react-router` v5 to v6 migration.
Review this matrix before running `npm install` during a React upgrade and before accepting an npm dependency conflict resolution, especially where concurrent mode compatibility may be affected.
## Core Upgrade Targets
| Package | React 17 (current) | React 18.3.1 (min) | React 19 (min) | Notes |
|---|---|---|---|---|
| `react` | 17.x | **18.3.1** | **19.0.0** | Pin exactly to 18.3.1 for the R18 orchestra |
| `react-dom` | 17.x | **18.3.1** | **19.0.0** | Must match react version exactly |
## Testing Libraries
| Package | React 18 Min | React 19 Min | Notes |
|---|---|---|---|
| `@testing-library/react` | **14.0.0** | **16.0.0** | RTL 13 uses ReactDOM.render internally - broken in R18 |
| `@testing-library/jest-dom` | **6.0.0** | **6.0.0** | v5 works but v6 has React 18 matcher updates |
| `@testing-library/user-event` | **14.0.0** | **14.0.0** | v13 is sync, v14 is async - API change required |
| `jest` | **27.x** | **27.x** | jest 27+ with jsdom 16+ for React 18 |
| `jest-environment-jsdom` | **27.x** | **27.x** | Must match jest version |
## Apollo Client
| Package | React 18 Min | React 19 Min | Notes |
|---|---|---|---|
| `@apollo/client` | **3.8.0** | **3.11.0** | 3.8 adds `useSyncExternalStore` for concurrent mode |
| `graphql` | **15.x** | **16.x** | Apollo 3.8+ peer requires graphql 15 or 16 |
Read **`references/apollo-details.md`** for concurrent mode issues and MockedProvider changes.
## Emotion
| Package | React 18 Min | React 19 Min | Notes |
|---|---|---|---|
| `@emotion/react` | **11.10.0** | **11.13.0** | 11.10 adds React 18 concurrent mode support |
| `@emotion/styled` | **11.10.0** | **11.13.0** | Must match @emotion/react version |
| `@emotion/cache` | **11.10.0** | **11.13.0** | If used directly |
## React Router
| Package | React 18 Min | React 19 Min | Notes |
|---|---|---|---|
| `react-router-dom` | **v6.0.0** | **v6.8.0** | v5 → v6 is a breaking migration - see details below |
| `react-router-dom` v5 | 5.3.4 (workaround) | ❌ Not supported | See legacy peer deps note |
**react-router v5 → v6 is a SEPARATE migration sprint.** Read `references/router-migration.md`.
## Redux
| Package | React 18 Min | React 19 Min | Notes |
|---|---|---|---|
| `react-redux` | **8.0.0** | **9.0.0** | v7 works on R18 legacy root only - breaks on concurrent mode |
| `redux` | **4.x** | **5.x** | Redux itself is framework-agnostic - react-redux version matters |
| `@reduxjs/toolkit` | **1.9.0** | **2.0.0** | RTK 1.9 tested against React 18 |
## Other Common Packages
| Package | React 18 Min | React 19 Min | Notes |
|---|---|---|---|
| `react-query` / `@tanstack/react-query` | **4.0.0** | **5.0.0** | v3 doesn't support concurrent mode |
| `react-hook-form` | **7.0.0** | **7.43.0** | v6 has concurrent mode issues |
| `formik` | **2.2.9** | **2.4.0** | v2.2.9 patched for React 18 |
| `react-select` | **5.0.0** | **5.8.0** | v4 has peer dep conflicts with R18 |
| `react-datepicker` | **4.8.0** | **6.0.0** | v4.8+ added React 18 support |
| `react-dnd` | **16.0.0** | **16.0.0** | v15 and below have R18 concurrent mode issues |
| `prop-types` | any | any | Standalone - unaffected by React version |
---
## Conflict Resolution Decision Tree
```
npm ls shows peer conflict for package X
Does package X have a version that supports React 18?
YES → npm install X@[min-compatible-version]
NO ↓
Is the package critical to the app?
YES → check GitHub issues for React 18 branch/fork
→ check if maintainer has a PR open
→ last resort: --legacy-peer-deps (document why)
NO → consider removing the package
```
## --legacy-peer-deps Rules
Only use `--legacy-peer-deps` when:
- The package has no React 18 compatible release
- The package is actively maintained (not abandoned)
- The conflict is only a peer dep declaration mismatch (not actual API incompatibility)
**Document every `--legacy-peer-deps` usage** in a comment at the top of package.json or in a MIGRATION.md file explaining why it was necessary.

View File

@@ -0,0 +1,68 @@
# Apollo Client - React 18 Compatibility Details
## Why Apollo 3.8+ is Required
Apollo Client 3.7 and below use an internal subscription model that is not compatible with React 18's concurrent rendering. In concurrent mode, React can interrupt and replay renders, which causes Apollo's store subscriptions to fire at incorrect times - producing stale data or missed updates.
Apollo 3.8 was the first version to adopt `useSyncExternalStore`, which React 18 requires for external stores to work correctly under concurrent rendering.
## Version Summary
| Apollo Version | React 18 Support | React 19 Support | Notes |
|---|---|---|---|
| < 3.7 | ❌ | ❌ | Concurrent mode data tearing |
| 3.7.x | ⚠️ | ⚠️ | Works with legacy root only (ReactDOM.render) |
| **3.8.x** | ✅ | ✅ | First fully compatible version |
| 3.9+ | ✅ | ✅ | Recommended |
| 3.11+ | ✅ | ✅ (confirmed) | Explicit React 19 testing added |
## If You're on Apollo 3.7 Using Legacy Root
If the app still uses `ReactDOM.render` (legacy root) and hasn't migrated to `createRoot` yet, Apollo 3.7 will technically work - but this means you're not getting any React 18 concurrent features (including automatic batching). This is a partial upgrade only.
As soon as `createRoot` is used, upgrade Apollo to 3.8+.
## MockedProvider in Tests - React 18
Apollo's `MockedProvider` works with React 18 but async behavior changed:
```jsx
// Old pattern - flushing with setTimeout:
await new Promise(resolve => setTimeout(resolve, 0));
wrapper.update();
// React 18 pattern - use waitFor or findBy:
await waitFor(() => {
expect(screen.getByText('Alice')).toBeInTheDocument();
});
// OR:
expect(await screen.findByText('Alice')).toBeInTheDocument();
```
## Upgrading Apollo
```bash
npm install @apollo/client@latest graphql@latest
```
If graphql peer dep conflicts with other packages:
```bash
npm ls graphql # check what version is being used
npm info @apollo/client peerDependencies # check what apollo requires
```
Apollo 3.8+ supports both `graphql@15` and `graphql@16`.
## InMemoryCache - No Changes Required
`InMemoryCache` configuration is unaffected by the React 18 upgrade. No migration needed for:
- `typePolicies`
- `fragmentMatcher`
- `possibleTypes`
- Custom field policies
## useQuery / useMutation / useSubscription - No Changes
Apollo hooks are unchanged in their API. The upgrade is entirely internal to how Apollo integrates with React's rendering model.

View File

@@ -0,0 +1,87 @@
# React Router v5 → v6 - Scope Assessment
## Why This Is a Separate Sprint
React Router v5 → v6 is a complete API rewrite. Unlike most React 18 upgrade steps which touch individual patterns, the router migration affects:
- Every `<Route>` component
- Every `<Switch>` (replaced by `<Routes>`)
- Every `useHistory()` (replaced by `useNavigate()`)
- Every `useRouteMatch()` (replaced by `useMatch()`)
- Every `<Redirect>` (replaced by `<Navigate>`)
- Nested route definitions (entirely new model)
- Route parameters access
- Query string handling
Attempting this as part of the React 18 upgrade sprint will scope-creep the migration significantly.
## Recommended Approach
### Option A - Defer Router Migration (Recommended)
Use `react-router-dom@5.3.4` with `--legacy-peer-deps` during the React 18 upgrade. This is explicitly documented as a supported workaround by the react-router team for React 18 compatibility on legacy root.
```bash
# In the React 18 dep surgeon:
npm install react-router-dom@5.3.4 --legacy-peer-deps
```
Document in package.json:
```json
"_legacyPeerDepsReason": {
"react-router-dom@5.3.4": "Router v5→v6 migration deferred to separate sprint. React 18 peer dep mismatch only - no API incompatibility on legacy root."
}
```
Then schedule the v5 → v6 migration as its own sprint after the React 18 upgrade is stable.
### Option B - Migrate Router as Part of React 18 Sprint
Only choose this if:
- The app has minimal routing (< 10 routes, no nested routes, no complex navigation logic)
- The team has bandwidth and the sprint timeline allows it
### Scope Assessment Scan
Run this to understand the router migration scope before deciding:
```bash
echo "=== Route definitions ==="
grep -rn "<Route\|<Switch\|<Redirect" src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." | wc -l
echo "=== useHistory calls ==="
grep -rn "useHistory()" src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." | wc -l
echo "=== useRouteMatch calls ==="
grep -rn "useRouteMatch()" src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." | wc -l
echo "=== withRouter HOC ==="
grep -rn "withRouter" src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." | wc -l
echo "=== history.push / history.replace ==="
grep -rn "history\.push\|history\.replace\|history\.go" src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." | wc -l
```
**Decision guide:**
- Total hits < 30 → router migration is feasible in this sprint
- Total hits 30100 → strongly recommend deferring
- Total hits > 100 → must defer - separate sprint required
## v5 → v6 API Changes Summary
| v5 | v6 | Notes |
|---|---|---|
| `<Switch>` | `<Routes>` | Direct replacement |
| `<Route path="/" component={C}>` | `<Route path="/" element={<C />}>` | element prop, not component |
| `<Route exact path="/">` | `<Route path="/">` | exact is default in v6 |
| `<Redirect to="/new">` | `<Navigate to="/new" />` | Component rename |
| `useHistory()` | `useNavigate()` | Returns a function, not an object |
| `history.push('/path')` | `navigate('/path')` | Direct call |
| `history.replace('/path')` | `navigate('/path', { replace: true })` | Options object |
| `useRouteMatch()` | `useMatch()` | Different return shape |
| `match.params` | `useParams()` | Hook instead of prop |
| Nested routes inline | Nested routes in config | Layout routes concept |
| `withRouter` HOC | `useNavigate` / `useParams` hooks | HOC removed |

View File

@@ -0,0 +1,93 @@
---
name: react18-enzyme-to-rtl
description: 'Provides exact Enzyme → React Testing Library migration patterns for React 18 upgrades. Use this skill whenever Enzyme tests need to be rewritten - shallow, mount, wrapper.find(), wrapper.simulate(), wrapper.prop(), wrapper.state(), wrapper.instance(), Enzyme configure/Adapter calls, or any test file that imports from enzyme. This skill covers the full API mapping and the philosophy shift from implementation testing to behavior testing. Always read this skill before rewriting Enzyme tests - do not translate Enzyme APIs 1:1, that produces brittle RTL tests.'
---
# React 18 Enzyme → RTL Migration
Enzyme has no React 18 adapter and no React 18 support path. All Enzyme tests must be rewritten using React Testing Library.
## The Philosophy Shift (Read This First)
Enzyme tests implementation. RTL tests behavior.
```jsx
// Enzyme: tests that the component has the right internal state
expect(wrapper.state('count')).toBe(3);
expect(wrapper.instance().handleClick).toBeDefined();
expect(wrapper.find('Button').prop('disabled')).toBe(true);
// RTL: tests what the user actually sees and can do
expect(screen.getByText('Count: 3')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /submit/i })).toBeDisabled();
```
This is not a 1:1 translation. Enzyme tests that verify internal state or instance methods don't have RTL equivalents - because RTL intentionally doesn't expose internals. **Rewrite the test to assert the visible outcome instead.**
## API Map
For complete before/after code for each Enzyme API, read:
- **`references/enzyme-api-map.md`** - full mapping: shallow, mount, find, simulate, prop, state, instance, configure
- **`references/async-patterns.md`** - waitFor, findBy, act(), Apollo MockedProvider, loading states, error states
## Core Rewrite Template
```jsx
// Every Enzyme test rewrites to this shape:
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import MyComponent from './MyComponent';
describe('MyComponent', () => {
it('does the thing', async () => {
// 1. Render (replaces shallow/mount)
render(<MyComponent prop="value" />);
// 2. Query (replaces wrapper.find())
const button = screen.getByRole('button', { name: /submit/i });
// 3. Interact (replaces simulate())
await userEvent.setup().click(button);
// 4. Assert on visible output (replaces wrapper.state() / wrapper.prop())
expect(screen.getByText('Submitted!')).toBeInTheDocument();
});
});
```
## RTL Query Priority (use in this order)
1. `getByRole` - matches accessible roles (button, textbox, heading, checkbox, etc.)
2. `getByLabelText` - form fields linked to labels
3. `getByPlaceholderText` - input placeholders
4. `getByText` - visible text content
5. `getByDisplayValue` - current value of input/select/textarea
6. `getByAltText` - image alt text
7. `getByTitle` - title attribute
8. `getByTestId` - `data-testid` attribute (last resort)
Prefer `getByRole` over `getByTestId`. It tests accessibility too.
## Wrapping with Providers
```jsx
// Enzyme with context:
const wrapper = mount(
<ApolloProvider client={client}>
<ThemeProvider theme={theme}>
<MyComponent />
</ThemeProvider>
</ApolloProvider>
);
// RTL equivalent (use your project's customRender or wrap inline):
import { render } from '@testing-library/react';
render(
<MockedProvider mocks={mocks} addTypename={false}>
<ThemeProvider theme={theme}>
<MyComponent />
</ThemeProvider>
</MockedProvider>
);
// Or use the project's customRender helper if it wraps providers
```

View File

@@ -0,0 +1,260 @@
# Async Test Patterns - Enzyme → RTL Migration
Reference for rewriting Enzyme async tests to React Testing Library with React 18 compatible patterns.
## The Core Problem
Enzyme's async tests typically used one of these approaches:
- `wrapper.update()` after state changes
- `setTimeout` / `Promise.resolve()` to flush microtasks
- `setImmediate` to flush async queues
- Direct instance method calls followed by `wrapper.update()`
None of these work in RTL. RTL provides `waitFor`, `findBy*`, and `act` instead.
---
## Pattern 1 - wrapper.update() After State Change
Enzyme required `wrapper.update()` to force a re-render after async state changes.
```jsx
// Enzyme:
it('loads data', async () => {
const wrapper = mount(<UserList />);
await Promise.resolve(); // flush microtasks
wrapper.update(); // force Enzyme to sync with DOM
expect(wrapper.find('li')).toHaveLength(3);
});
```
```jsx
// RTL - waitFor handles re-renders automatically:
import { render, screen, waitFor } from '@testing-library/react';
it('loads data', async () => {
render(<UserList />);
await waitFor(() => {
expect(screen.getAllByRole('listitem')).toHaveLength(3);
});
});
```
---
## Pattern 2 - Async Action Triggered by User Interaction
```jsx
// Enzyme:
it('fetches user on button click', async () => {
const wrapper = mount(<UserCard />);
wrapper.find('button').simulate('click');
await new Promise(resolve => setTimeout(resolve, 0));
wrapper.update();
expect(wrapper.find('.user-name').text()).toBe('John Doe');
});
```
```jsx
// RTL:
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
it('fetches user on button click', async () => {
render(<UserCard />);
await userEvent.setup().click(screen.getByRole('button', { name: /load/i }));
// findBy* auto-waits up to 1000ms (configurable)
expect(await screen.findByText('John Doe')).toBeInTheDocument();
});
```
---
## Pattern 3 - Loading State Assertion
```jsx
// Enzyme - asserted loading state synchronously then final state after flush:
it('shows loading then result', async () => {
const wrapper = mount(<SearchResults query="react" />);
expect(wrapper.find('.spinner').exists()).toBe(true);
await new Promise(resolve => setTimeout(resolve, 100));
wrapper.update();
expect(wrapper.find('.spinner').exists()).toBe(false);
expect(wrapper.find('.result')).toHaveLength(5);
});
```
```jsx
// RTL:
it('shows loading then result', async () => {
render(<SearchResults query="react" />);
// Loading state - check it appears
expect(screen.getByRole('progressbar')).toBeInTheDocument();
// Or if loading is text:
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// Wait for results to appear (loading disappears, results show)
await waitFor(() => {
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
});
expect(screen.getAllByRole('listitem')).toHaveLength(5);
});
```
---
## Pattern 4 - Apollo MockedProvider Async Tests
```jsx
// Enzyme with Apollo - used to flush with multiple ticks:
it('renders user from query', async () => {
const wrapper = mount(
<MockedProvider mocks={mocks} addTypename={false}>
<UserProfile id="1" />
</MockedProvider>
);
await new Promise(resolve => setTimeout(resolve, 0)); // flush Apollo queue
wrapper.update();
expect(wrapper.find('.username').text()).toBe('Alice');
});
```
```jsx
// RTL with Apollo:
import { render, screen, waitFor } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
it('renders user from query', async () => {
render(
<MockedProvider mocks={mocks} addTypename={false}>
<UserProfile id="1" />
</MockedProvider>
);
// Wait for Apollo to resolve the query
expect(await screen.findByText('Alice')).toBeInTheDocument();
// OR:
await waitFor(() => {
expect(screen.getByText('Alice')).toBeInTheDocument();
});
});
```
**Apollo loading state in RTL:**
```jsx
it('shows loading then data', async () => {
render(
<MockedProvider mocks={mocks} addTypename={false}>
<UserProfile id="1" />
</MockedProvider>
);
// Apollo loading state - check immediately after render
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// Then wait for data
expect(await screen.findByText('Alice')).toBeInTheDocument();
});
```
---
## Pattern 5 - Error State from Async Operation
```jsx
// Enzyme:
it('shows error on failed fetch', async () => {
server.use(rest.get('/api/user', (req, res, ctx) => res(ctx.status(500))));
const wrapper = mount(<UserCard />);
wrapper.find('button').simulate('click');
await new Promise(resolve => setTimeout(resolve, 0));
wrapper.update();
expect(wrapper.find('.error-message').text()).toContain('Something went wrong');
});
```
```jsx
// RTL:
it('shows error on failed fetch', async () => {
// (assuming MSW or jest.mock for fetch)
render(<UserCard />);
await userEvent.setup().click(screen.getByRole('button', { name: /load/i }));
expect(await screen.findByText(/something went wrong/i)).toBeInTheDocument();
});
```
---
## Pattern 6 - act() for Manual Async Control
When you need explicit control over async timing (rare with RTL but occasionally needed for class component tests):
```jsx
// RTL with act() for fine-grained async control:
import { act } from 'react';
it('handles sequential state updates', async () => {
render(<MultiStepForm />);
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: /next/i }));
await Promise.resolve(); // flush microtask queue
});
expect(screen.getByText('Step 2')).toBeInTheDocument();
});
```
---
## RTL Async Query Guide
| Method | Behavior | Use when |
|---|---|---|
| `getBy*` | Synchronous - throws if not found | Element is always present immediately |
| `queryBy*` | Synchronous - returns null if not found | Checking element does NOT exist |
| `findBy*` | Async - waits up to 1000ms, rejects if not found | Element appears asynchronously |
| `getAllBy*` | Synchronous - throws if 0 found | Multiple elements always present |
| `queryAllBy*` | Synchronous - returns [] if none found | Checking count or non-existence |
| `findAllBy*` | Async - waits for elements to appear | Multiple elements appear asynchronously |
| `waitFor(fn)` | Retries fn until no error or timeout | Custom assertion that needs polling |
| `waitForElementToBeRemoved(el)` | Waits until element disappears | Loading states, removals |
**Default timeout:** 1000ms. Configure globally in `jest.config.js`:
```js
// Increase timeout for slow CI environments
// jest.config.js
module.exports = {
testEnvironmentOptions: {
asyncUtilTimeout: 3000,
},
};
```
---
## Common Migration Mistakes
```jsx
// WRONG - mixing async query with sync assertion:
const el = await screen.findByText('Result');
// el is already resolved here - findBy returns the element, not a promise
expect(await el).toBeInTheDocument(); // unnecessary second await
// CORRECT:
const el = await screen.findByText('Result');
expect(el).toBeInTheDocument();
// OR simply:
expect(await screen.findByText('Result')).toBeInTheDocument();
```
```jsx
// WRONG - using getBy* for elements that appear asynchronously:
fireEvent.click(button);
expect(screen.getByText('Loaded!')).toBeInTheDocument(); // throws before data loads
// CORRECT:
fireEvent.click(button);
expect(await screen.findByText('Loaded!')).toBeInTheDocument(); // waits
```

View File

@@ -0,0 +1,224 @@
# Enzyme API Map - Complete Before/After
## Setup / Configure
```jsx
// Enzyme:
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new Adapter() });
// RTL: delete this entirely - no setup needed
// (jest.config.js setupFilesAfterFramework handles @testing-library/jest-dom matchers)
```
---
## Rendering
```jsx
// Enzyme - shallow (no children rendered):
import { shallow } from 'enzyme';
const wrapper = shallow(<MyComponent prop="value" />);
// RTL - render (full render, children included):
import { render } from '@testing-library/react';
render(<MyComponent prop="value" />);
// No wrapper variable needed - query via screen
```
```jsx
// Enzyme - mount (full render with DOM):
import { mount } from 'enzyme';
const wrapper = mount(<MyComponent />);
// RTL - same render() call handles this
render(<MyComponent />);
```
---
## Querying
```jsx
// Enzyme - find by component type:
const button = wrapper.find('button');
const comp = wrapper.find(ChildComponent);
const items = wrapper.find('.list-item');
// RTL - query by accessible attributes:
const button = screen.getByRole('button');
const button = screen.getByRole('button', { name: /submit/i });
const heading = screen.getByRole('heading', { name: /title/i });
const input = screen.getByLabelText('Email');
const items = screen.getAllByRole('listitem');
```
```jsx
// Enzyme - find by text:
wrapper.find('.message').text() === 'Hello'
// RTL:
screen.getByText('Hello')
screen.getByText(/hello/i) // case-insensitive regex
```
---
## User Interaction
```jsx
// Enzyme:
wrapper.find('button').simulate('click');
wrapper.find('input').simulate('change', { target: { value: 'hello' } });
wrapper.find('form').simulate('submit');
// RTL - fireEvent (synchronous, low-level):
import { fireEvent } from '@testing-library/react';
fireEvent.click(screen.getByRole('button'));
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'hello' } });
fireEvent.submit(screen.getByRole('form'));
// RTL - userEvent (preferred, simulates real user behavior):
import userEvent from '@testing-library/user-event';
const user = userEvent.setup();
await user.click(screen.getByRole('button'));
await user.type(screen.getByRole('textbox'), 'hello');
await user.selectOptions(screen.getByRole('combobox'), 'option1');
```
**Use `userEvent` for most interactions** - it fires the full event sequence (pointerdown, mousedown, focus, click, etc.) like a real user. Use `fireEvent` only when testing specific event properties.
---
## Assertions on Props and State
```jsx
// Enzyme - prop assertion:
expect(wrapper.find('input').prop('disabled')).toBe(true);
expect(wrapper.prop('className')).toContain('active');
// RTL - assert on visible attributes:
expect(screen.getByRole('textbox')).toBeDisabled();
expect(screen.getByRole('button')).toHaveAttribute('type', 'submit');
expect(screen.getByRole('listitem')).toHaveClass('active');
```
```jsx
// Enzyme - state assertion (NO RTL EQUIVALENT):
expect(wrapper.state('count')).toBe(3);
expect(wrapper.state('loading')).toBe(false);
// RTL - assert on what the state renders:
expect(screen.getByText('Count: 3')).toBeInTheDocument();
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
```
**Key principle:** Don't test state values - test what the state produces in the UI. If the component renders `<span>Count: {this.state.count}</span>`, test that span.
---
## Instance Methods
```jsx
// Enzyme - direct method call (NO RTL EQUIVALENT):
wrapper.instance().handleSubmit();
wrapper.instance().loadData();
// RTL - trigger through the UI:
await userEvent.setup().click(screen.getByRole('button', { name: /submit/i }));
// Or if no UI trigger exists, reconsider: should internal methods be tested directly?
// Usually the answer is no - test the rendered outcome instead.
```
---
## Existence Checks
```jsx
// Enzyme:
expect(wrapper.find('.error')).toHaveLength(1);
expect(wrapper.find('.error')).toHaveLength(0);
expect(wrapper.exists('.error')).toBe(true);
// RTL:
expect(screen.getByText('Error message')).toBeInTheDocument();
expect(screen.queryByText('Error message')).not.toBeInTheDocument();
// queryBy returns null instead of throwing when not found
// getBy throws if not found - use in positive assertions
// findBy returns a promise - use for async elements
```
---
## Multiple Elements
```jsx
// Enzyme:
expect(wrapper.find('li')).toHaveLength(5);
wrapper.find('li').forEach((item, i) => {
expect(item.text()).toBe(expectedItems[i]);
});
// RTL:
const items = screen.getAllByRole('listitem');
expect(items).toHaveLength(5);
items.forEach((item, i) => {
expect(item).toHaveTextContent(expectedItems[i]);
});
```
---
## Before/After: Complete Component Test
```jsx
// Enzyme version:
import { shallow } from 'enzyme';
describe('LoginForm', () => {
it('submits with credentials', () => {
const mockSubmit = jest.fn();
const wrapper = shallow(<LoginForm onSubmit={mockSubmit} />);
wrapper.find('input[name="email"]').simulate('change', {
target: { value: 'user@example.com' }
});
wrapper.find('input[name="password"]').simulate('change', {
target: { value: 'password123' }
});
wrapper.find('button[type="submit"]').simulate('click');
expect(wrapper.state('loading')).toBe(true);
expect(mockSubmit).toHaveBeenCalledWith({
email: 'user@example.com',
password: 'password123'
});
});
});
```
```jsx
// RTL version:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('LoginForm', () => {
it('submits with credentials', async () => {
const mockSubmit = jest.fn();
const user = userEvent.setup();
render(<LoginForm onSubmit={mockSubmit} />);
await user.type(screen.getByLabelText(/email/i), 'user@example.com');
await user.type(screen.getByLabelText(/password/i), 'password123');
await user.click(screen.getByRole('button', { name: /submit/i }));
// Assert on visible output - not on state
expect(screen.getByRole('button', { name: /submit/i })).toBeDisabled(); // loading state
expect(mockSubmit).toHaveBeenCalledWith({
email: 'user@example.com',
password: 'password123'
});
});
});
```

View File

@@ -0,0 +1,47 @@
---
name: react18-legacy-context
description: 'Provides the complete migration pattern for React legacy context API (contextTypes, childContextTypes, getChildContext) to the modern createContext API. Use this skill whenever migrating legacy context in class components - this is always a cross-file migration requiring the provider AND all consumers to be updated together. Use it before touching any contextTypes or childContextTypes code, because migrating only the provider without the consumers (or vice versa) will cause a runtime failure. Always read this skill before writing any context migration - the cross-file coordination steps here prevent the most common context migration bugs.'
---
# React 18 Legacy Context Migration
Legacy context (`contextTypes`, `childContextTypes`, `getChildContext`) was deprecated in React 16.3 and warns in React 18.3.1. It is **removed in React 19**.
## This Is Always a Cross-File Migration
Unlike most other migrations that touch one file at a time, context migration requires coordinating:
1. Create the context object (usually a new file)
2. Update the **provider** component
3. Update **every consumer** component
Missing any consumer leaves the app broken - it will read from the wrong context or get `undefined`.
## Migration Steps (Always Follow This Order)
```
Step 1: Find the provider (childContextTypes + getChildContext)
Step 2: Find ALL consumers (contextTypes)
Step 3: Create the context file
Step 4: Update the provider
Step 5: Update each consumer (class components → contextType, function components → useContext)
Step 6: Verify - run the app, check no legacy context warnings remain
```
## Scan Commands
```bash
# Find all providers
grep -rn "childContextTypes\|getChildContext" src/ --include="*.js" --include="*.jsx" | grep -v "\.test\."
# Find all consumers
grep -rn "contextTypes\s*=" src/ --include="*.js" --include="*.jsx" | grep -v "\.test\."
# Find this.context usage (may be legacy or modern - check which)
grep -rn "this\.context\." src/ --include="*.js" --include="*.jsx" | grep -v "\.test\."
```
## Reference Files
- **`references/single-context.md`** - complete migration for one context (theme, auth, etc.) with provider + class consumer + function consumer
- **`references/multi-context.md`** - apps with multiple legacy contexts (nested providers, multiple consumers of different contexts)
- **`references/context-file-template.md`** - the standard file structure for a new context module

View File

@@ -0,0 +1,116 @@
# Context File Template
Standard template for a new context module. Copy and fill in the name.
## Template
```jsx
// src/contexts/[Name]Context.js
import React from 'react';
// ─── 1. Default Value ───────────────────────────────────────────────────────
// Shape must match what the provider will pass as `value`
// Used when a consumer renders outside any provider (edge case protection)
const defaultValue = {
// fill in the shape
};
// ─── 2. Create Context ──────────────────────────────────────────────────────
export const [Name]Context = React.createContext(defaultValue);
// ─── 3. Display Name (for React DevTools) ───────────────────────────────────
[Name]Context.displayName = '[Name]Context';
// ─── 4. Optional: Custom Hook (strongly recommended) ────────────────────────
// Provides a clean import path and a helpful error if used outside provider
export function use[Name]() {
const context = React.useContext([Name]Context);
if (context === defaultValue) {
// Only throw if defaultValue is a sentinel - skip if a real default makes sense
// throw new Error('use[Name] must be used inside a [Name]Provider');
}
return context;
}
```
## Filled Example - AuthContext
```jsx
// src/contexts/AuthContext.js
import React from 'react';
const defaultValue = {
user: null,
isAuthenticated: false,
login: () => Promise.resolve(),
logout: () => {},
};
export const AuthContext = React.createContext(defaultValue);
AuthContext.displayName = 'AuthContext';
export function useAuth() {
return React.useContext(AuthContext);
}
```
## Filled Example - ThemeContext
```jsx
// src/contexts/ThemeContext.js
import React from 'react';
const defaultValue = {
theme: 'light',
toggleTheme: () => {},
};
export const ThemeContext = React.createContext(defaultValue);
ThemeContext.displayName = 'ThemeContext';
export function useTheme() {
return React.useContext(ThemeContext);
}
```
## Where to Put Context Files
```
src/
contexts/ ← preferred: dedicated folder
AuthContext.js
ThemeContext.js
```
Alternative acceptable locations:
```
src/context/ ← singular is also fine
src/store/contexts/ ← if co-located with state management
```
Do NOT put context files inside a component folder - contexts are cross-cutting and shouldn't be owned by any one component.
## Provider Placement in the App
Context providers wrap the components that need access. Place as low in the tree as possible, not always at root:
```jsx
// App.js
import { ThemeProvider } from './ThemeProvider';
import { AuthProvider } from './AuthProvider';
function App() {
return (
// Auth wraps everything - login state is needed everywhere
<AuthProvider>
{/* Theme wraps only the UI shell - not needed in pure data providers */}
<ThemeProvider>
<Router>
<AppShell />
</Router>
</ThemeProvider>
</AuthProvider>
);
}
```

View File

@@ -0,0 +1,195 @@
# Multiple Legacy Contexts - Migration Reference
## Identifying Multiple Contexts
A React 16/17 codebase often has several legacy contexts used for different concerns:
```bash
# Find distinct context names used in childContextTypes
grep -rn "childContextTypes" src/ --include="*.js" --include="*.jsx" | grep -v "\.test\."
# Each hit is a separate context to migrate
```
Common patterns in class-heavy codebases:
- **Theme context** - dark/light mode, color palette
- **Auth context** - current user, login/logout functions
- **Router context** - current route, navigation (if using older react-router)
- **Store context** - Redux store, dispatch (if using older connect patterns)
- **Locale/i18n context** - language, translation function
- **Toast/notification context** - show/hide notifications
---
## Migration Order
Migrate contexts one at a time. Each is an independent migration:
```
For each legacy context:
1. Create src/contexts/[Name]Context.js
2. Update the provider
3. Update all consumers
4. Run the app - verify no warning for this context
5. Move to the next context
```
Do not migrate all providers first then all consumers - it leaves the app in a broken intermediate state.
---
## Multiple Contexts in the Same Provider
Some apps combined multiple contexts in one provider component:
```jsx
// Before - one provider exports multiple context values:
class AppProvider extends React.Component {
static childContextTypes = {
theme: PropTypes.string,
user: PropTypes.object,
locale: PropTypes.string,
notifications: PropTypes.array,
};
getChildContext() {
return {
theme: this.state.theme,
user: this.state.user,
locale: this.state.locale,
notifications: this.state.notifications,
};
}
}
```
**Migration approach - split into separate contexts:**
```jsx
// src/contexts/ThemeContext.js
export const ThemeContext = React.createContext('light');
// src/contexts/AuthContext.js
export const AuthContext = React.createContext({ user: null, login: () => {}, logout: () => {} });
// src/contexts/LocaleContext.js
export const LocaleContext = React.createContext('en');
// src/contexts/NotificationContext.js
export const NotificationContext = React.createContext([]);
```
```jsx
// AppProvider.js - now wraps with multiple providers
import { ThemeContext } from './contexts/ThemeContext';
import { AuthContext } from './contexts/AuthContext';
import { LocaleContext } from './contexts/LocaleContext';
import { NotificationContext } from './contexts/NotificationContext';
class AppProvider extends React.Component {
render() {
const { theme, user, locale, notifications } = this.state;
return (
<ThemeContext.Provider value={theme}>
<AuthContext.Provider value={{ user, login: this.login, logout: this.logout }}>
<LocaleContext.Provider value={locale}>
<NotificationContext.Provider value={notifications}>
{this.props.children}
</NotificationContext.Provider>
</LocaleContext.Provider>
</AuthContext.Provider>
</ThemeContext.Provider>
);
}
}
```
---
## Consumer With Multiple Contexts (Class Component)
Class components can only use ONE `static contextType`. For multiple, use `Consumer` render props or convert to a function component.
### Option A - Render Props (keep as class component)
```jsx
import { ThemeContext } from '../contexts/ThemeContext';
import { AuthContext } from '../contexts/AuthContext';
class UserPanel extends React.Component {
render() {
return (
<ThemeContext.Consumer>
{(theme) => (
<AuthContext.Consumer>
{({ user, logout }) => (
<div className={`panel panel-${theme}`}>
<span>{user?.name}</span>
<button onClick={logout}>Sign out</button>
</div>
)}
</AuthContext.Consumer>
)}
</ThemeContext.Consumer>
);
}
}
```
### Option B - Convert to Function Component (preferred)
```jsx
import { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';
import { AuthContext } from '../contexts/AuthContext';
function UserPanel() {
const theme = useContext(ThemeContext);
const { user, logout } = useContext(AuthContext);
return (
<div className={`panel panel-${theme}`}>
<span>{user?.name}</span>
<button onClick={logout}>Sign out</button>
</div>
);
}
```
If converting to a function component is out of scope for this migration sprint - use Option A. If the class component is simple (mostly just render), Option B is worth the minor rewrite.
---
## Context File Naming Conventions
Use consistent naming across the codebase:
```
src/
contexts/
ThemeContext.js → exports: ThemeContext, ThemeProvider (optional)
AuthContext.js → exports: AuthContext, AuthProvider (optional)
LocaleContext.js → exports: LocaleContext
```
Each file exports the context object. The provider can stay in its original file and just import the context.
---
## Verification After All Contexts Migrated
```bash
# Should return zero hits for legacy context patterns
echo "=== childContextTypes ==="
grep -rn "childContextTypes" src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." | wc -l
echo "=== contextTypes (legacy) ==="
grep -rn "^\s*static contextTypes\s*=\|contextTypes\.propTypes" src/ --include="*.js" | grep -v "\.test\." | wc -l
echo "=== getChildContext ==="
grep -rn "getChildContext" src/ --include="*.js" --include="*.jsx" | grep -v "\.test\." | wc -l
echo "All three should be 0"
```
Note: `static contextType` (singular) is the MODERN API - that's correct. Only `contextTypes` (plural) is legacy.

View File

@@ -0,0 +1,224 @@
# Single Context Migration - Complete Before/After
## Full Example: ThemeContext
This covers the most common pattern - one context with one provider and multiple consumers.
---
### Step 1 - Before State (Legacy)
**ThemeProvider.js (provider):**
```jsx
import PropTypes from 'prop-types';
class ThemeProvider extends React.Component {
static childContextTypes = {
theme: PropTypes.string,
toggleTheme: PropTypes.func,
};
state = { theme: 'light' };
toggleTheme = () => {
this.setState(s => ({ theme: s.theme === 'light' ? 'dark' : 'light' }));
};
getChildContext() {
return {
theme: this.state.theme,
toggleTheme: this.toggleTheme,
};
}
render() {
return this.props.children;
}
}
```
**ThemedButton.js (class consumer):**
```jsx
import PropTypes from 'prop-types';
class ThemedButton extends React.Component {
static contextTypes = {
theme: PropTypes.string,
toggleTheme: PropTypes.func,
};
render() {
const { theme, toggleTheme } = this.context;
return (
<button className={`btn btn-${theme}`} onClick={toggleTheme}>
Toggle Theme
</button>
);
}
}
```
**ThemedHeader.js (function consumer - if any):**
```jsx
// Function components couldn't use legacy context cleanly
// They had to use a class wrapper or render prop
```
---
### Step 2 - Create Context File
**src/contexts/ThemeContext.js (new file):**
```jsx
import React from 'react';
// Default value matches the shape of getChildContext() return
export const ThemeContext = React.createContext({
theme: 'light',
toggleTheme: () => {},
});
// Named export for the context - both provider and consumers import from here
```
---
### Step 3 - Update Provider
**ThemeProvider.js (after):**
```jsx
import React from 'react';
import { ThemeContext } from '../contexts/ThemeContext';
class ThemeProvider extends React.Component {
state = { theme: 'light' };
toggleTheme = () => {
this.setState(s => ({ theme: s.theme === 'light' ? 'dark' : 'light' }));
};
render() {
// React 19 JSX shorthand: <ThemeContext value={...}>
// React 18: <ThemeContext.Provider value={...}>
return (
<ThemeContext.Provider
value={{
theme: this.state.theme,
toggleTheme: this.toggleTheme,
}}
>
{this.props.children}
</ThemeContext.Provider>
);
}
}
export default ThemeProvider;
```
> **React 19 note:** In React 19 you can write `<ThemeContext value={...}>` directly (no `.Provider`). For React 18.3.1 use `<ThemeContext.Provider value={...}>`.
---
### Step 4 - Update Class Consumer
**ThemedButton.js (after):**
```jsx
import React from 'react';
import { ThemeContext } from '../contexts/ThemeContext';
class ThemedButton extends React.Component {
// singular contextType (not contextTypes)
static contextType = ThemeContext;
render() {
const { theme, toggleTheme } = this.context;
return (
<button className={`btn btn-${theme}`} onClick={toggleTheme}>
Toggle Theme
</button>
);
}
}
export default ThemedButton;
```
**Key differences from legacy:**
- `static contextType` (singular) not `contextTypes` (plural)
- No PropTypes declaration needed
- `this.context` is the full value object (not a partial - whatever you passed to `value`)
- Only ONE context per class component via `contextType` - use `Context.Consumer` render prop for multiple
---
### Step 5 - Update Function Consumer
**ThemedHeader.js (after - now straightforward with hooks):**
```jsx
import { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';
function ThemedHeader({ title }) {
const { theme } = useContext(ThemeContext);
return <h1 className={`header-${theme}`}>{title}</h1>;
}
```
---
### Step 6 - Multiple Contexts in One Class Component
If a class component consumed more than one legacy context, it gets complex. Class components can only have one `static contextType`. For multiple contexts, use the render prop form:
```jsx
import { ThemeContext } from '../contexts/ThemeContext';
import { AuthContext } from '../contexts/AuthContext';
class Dashboard extends React.Component {
render() {
return (
<ThemeContext.Consumer>
{({ theme }) => (
<AuthContext.Consumer>
{({ user }) => (
<div className={`dashboard-${theme}`}>
Welcome, {user.name}
</div>
)}
</AuthContext.Consumer>
)}
</ThemeContext.Consumer>
);
}
}
```
Or consider migrating the class component to a function component to use `useContext` cleanly.
---
### Verification Checklist
After migrating one context:
```bash
# Provider - no legacy context exports remain
grep -n "childContextTypes\|getChildContext" src/ThemeProvider.js
# Consumers - no legacy context consumption remains
grep -rn "contextTypes\s*=" src/ --include="*.js" --include="*.jsx" | grep -v "ThemeContext\|\.test\."
# this.context usage - confirm it reads from contextType not legacy
grep -rn "this\.context\." src/ --include="*.js" | grep -v "\.test\."
```
Each should return zero hits for the migrated context.

View File

@@ -0,0 +1,63 @@
---
name: react18-lifecycle-patterns
description: 'Provides exact before/after migration patterns for the three unsafe class component lifecycle methods - componentWillMount, componentWillReceiveProps, and componentWillUpdate - targeting React 18.3.1. Use this skill whenever a class component needs its lifecycle methods migrated, when deciding between getDerivedStateFromProps vs componentDidUpdate, when adding getSnapshotBeforeUpdate, or when fixing React 18 UNSAFE_ lifecycle warnings. Always use this skill before writing any lifecycle migration code - do not guess the pattern from memory, the decision trees here prevent the most common migration mistakes.'
---
# React 18 Lifecycle Patterns
Reference for migrating the three unsafe class component lifecycle methods to React 18.3.1 compliant patterns.
## Quick Decision Guide
Before migrating any lifecycle method, identify the **semantic category** of what the method does. Wrong category = wrong migration. The table below routes you to the correct reference file.
### componentWillMount - what does it do?
| What it does | Correct migration | Reference |
|---|---|---|
| Sets initial state (`this.setState(...)`) | Move to `constructor` | [→ componentWillMount.md](references/componentWillMount.md#case-a) |
| Runs a side effect (fetch, subscription, DOM) | Move to `componentDidMount` | [→ componentWillMount.md](references/componentWillMount.md#case-b) |
| Derives initial state from props | Move to `constructor` with props | [→ componentWillMount.md](references/componentWillMount.md#case-c) |
### componentWillReceiveProps - what does it do?
| What it does | Correct migration | Reference |
|---|---|---|
| Async side effect triggered by prop change (fetch, cancel) | `componentDidUpdate` | [→ componentWillReceiveProps.md](references/componentWillReceiveProps.md#case-a) |
| Pure state derivation from new props (no side effects) | `getDerivedStateFromProps` | [→ componentWillReceiveProps.md](references/componentWillReceiveProps.md#case-b) |
### componentWillUpdate - what does it do?
| What it does | Correct migration | Reference |
|---|---|---|
| Reads the DOM before update (scroll, size, position) | `getSnapshotBeforeUpdate` | [→ componentWillUpdate.md](references/componentWillUpdate.md#case-a) |
| Cancels requests / runs effects before update | `componentDidUpdate` with prev comparison | [→ componentWillUpdate.md](references/componentWillUpdate.md#case-b) |
---
## The UNSAFE_ Prefix Rule
**Never use `UNSAFE_componentWillMount`, `UNSAFE_componentWillReceiveProps`, or `UNSAFE_componentWillUpdate` as a permanent fix.**
Prefixing suppresses the React 18.3.1 warning but does NOT:
- Fix concurrent mode safety issues
- Prepare the codebase for React 19 (where these are removed, with or without the prefix)
- Fix the underlying semantic problem the migration is meant to address
The UNSAFE_ prefix is only appropriate as a temporary hold while scheduling the real migration sprint. Mark any UNSAFE_ prefix additions with:
```jsx
// TODO: React 19 will remove this. Migrate before React 19 upgrade.
// UNSAFE_ prefix added temporarily - replace with componentDidMount / getDerivedStateFromProps / etc.
```
---
## Reference Files
Read the full reference file for the lifecycle method you are migrating:
- **`references/componentWillMount.md`** - 3 cases with full before/after code
- **`references/componentWillReceiveProps.md`** - getDerivedStateFromProps trap warnings, full examples
- **`references/componentWillUpdate.md`** - getSnapshotBeforeUpdate + componentDidUpdate pairing
Read the relevant file before writing any migration code.

View File

@@ -0,0 +1,155 @@
# componentWillMount Migration Reference
## Case A - Initializes State {#case-a}
The method only calls `this.setState()` with static or computed values that do not depend on async operations.
**Before:**
```jsx
class UserList extends React.Component {
componentWillMount() {
this.setState({ items: [], loading: false, page: 1 });
}
render() { ... }
}
```
**After - move to constructor:**
```jsx
class UserList extends React.Component {
constructor(props) {
super(props);
this.state = { items: [], loading: false, page: 1 };
}
render() { ... }
}
```
**If constructor already exists**, merge the state:
```jsx
class UserList extends React.Component {
constructor(props) {
super(props);
// Existing state merged with componentWillMount state:
this.state = {
...this.existingState, // whatever was already here
items: [],
loading: false,
page: 1,
};
}
}
```
---
## Case B - Runs a Side Effect {#case-b}
The method fetches data, sets up subscriptions, interacts with external APIs, or touches the DOM.
**Before:**
```jsx
class UserDashboard extends React.Component {
componentWillMount() {
this.subscription = this.props.eventBus.subscribe(this.handleEvent);
fetch(`/api/users/${this.props.userId}`)
.then(r => r.json())
.then(user => this.setState({ user, loading: false }));
this.setState({ loading: true });
}
}
```
**After - move to componentDidMount:**
```jsx
class UserDashboard extends React.Component {
constructor(props) {
super(props);
this.state = { loading: true, user: null }; // initial state here
}
componentDidMount() {
// All side effects move here - runs after first render
this.subscription = this.props.eventBus.subscribe(this.handleEvent);
fetch(`/api/users/${this.props.userId}`)
.then(r => r.json())
.then(user => this.setState({ user, loading: false }));
}
componentWillUnmount() {
// Always pair subscriptions with cleanup
this.subscription?.unsubscribe();
}
}
```
**Why this is safe:** In React 18 concurrent mode, `componentWillMount` can be called multiple times before mounting. Side effects inside it can fire multiple times. `componentDidMount` is guaranteed to fire exactly once after mount.
---
## Case C - Derives Initial State from Props {#case-c}
The method reads `this.props` to compute an initial state value.
**Before:**
```jsx
class PriceDisplay extends React.Component {
componentWillMount() {
this.setState({
formattedPrice: `$${this.props.price.toFixed(2)}`,
isDiscount: this.props.price < this.props.originalPrice,
});
}
}
```
**After - constructor with props:**
```jsx
class PriceDisplay extends React.Component {
constructor(props) {
super(props);
this.state = {
formattedPrice: `$${props.price.toFixed(2)}`,
isDiscount: props.price < props.originalPrice,
};
}
}
```
**Note:** If this initial state needs to UPDATE when props change later, that's a `getDerivedStateFromProps` case - see `componentWillReceiveProps.md` Case B.
---
## Multiple Patterns in One Method
If a single `componentWillMount` does both state init AND side effects:
```jsx
// Mixed - state init + fetch
componentWillMount() {
this.setState({ loading: true, items: [] }); // Case A
fetch('/api/items').then(r => r.json()) // Case B
.then(items => this.setState({ items, loading: false }));
}
```
Split them:
```jsx
constructor(props) {
super(props);
this.state = { loading: true, items: [] }; // Case A → constructor
}
componentDidMount() {
fetch('/api/items').then(r => r.json()) // Case B → componentDidMount
.then(items => this.setState({ items, loading: false }));
}
```

View File

@@ -0,0 +1,164 @@
# componentWillReceiveProps Migration Reference
## The Core Decision
```
Does componentWillReceiveProps trigger async work or side effects?
YES → componentDidUpdate
NO (pure state derivation only) → getDerivedStateFromProps
```
When in doubt: use `componentDidUpdate`. It's always safe.
`getDerivedStateFromProps` has traps (see bottom of this file) that make it the wrong choice when the logic is anything other than purely synchronous state derivation.
---
## Case A - Async Side Effects / Fetch on Prop Change {#case-a}
The method fetches data, cancels requests, updates external state, or runs any async operation when a prop changes.
**Before:**
```jsx
class UserProfile extends React.Component {
componentWillReceiveProps(nextProps) {
if (nextProps.userId !== this.props.userId) {
this.setState({ loading: true, profile: null });
fetchProfile(nextProps.userId)
.then(profile => this.setState({ profile, loading: false }))
.catch(err => this.setState({ error: err, loading: false }));
}
}
}
```
**After - componentDidUpdate:**
```jsx
class UserProfile extends React.Component {
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
// Use this.props (not nextProps - the update already happened)
this.setState({ loading: true, profile: null });
fetchProfile(this.props.userId)
.then(profile => this.setState({ profile, loading: false }))
.catch(err => this.setState({ error: err, loading: false }));
}
}
}
```
**Key difference:** `componentDidUpdate` receives `prevProps` - you compare `prevProps.x !== this.props.x` instead of `this.props.x !== nextProps.x`. The update has already applied.
**Cancellation pattern** (important for async):
```jsx
class UserProfile extends React.Component {
_requestId = 0;
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
const requestId = ++this._requestId;
this.setState({ loading: true });
fetchProfile(this.props.userId).then(profile => {
// Ignore stale responses if userId changed again
if (requestId === this._requestId) {
this.setState({ profile, loading: false });
}
});
}
}
}
```
---
## Case B - Pure State Derivation from Props {#case-b}
The method only derives state values from the new props synchronously. No async work, no side effects, no external calls.
**Before:**
```jsx
class SortedList extends React.Component {
componentWillReceiveProps(nextProps) {
if (nextProps.items !== this.props.items) {
this.setState({
sortedItems: [...nextProps.items].sort((a, b) => a.name.localeCompare(b.name)),
});
}
}
}
```
**After - getDerivedStateFromProps:**
```jsx
class SortedList extends React.Component {
// Must track previous prop to detect changes
static getDerivedStateFromProps(props, state) {
if (props.items !== state.prevItems) {
return {
sortedItems: [...props.items].sort((a, b) => a.name.localeCompare(b.name)),
prevItems: props.items, // ← always store the prop you're comparing
};
}
return null; // null = no state change
}
constructor(props) {
super(props);
this.state = {
sortedItems: [...props.items].sort((a, b) => a.name.localeCompare(b.name)),
prevItems: props.items, // ← initialize in constructor too
};
}
}
```
---
## getDerivedStateFromProps - Traps and Warnings
### Trap 1: It fires on EVERY render, not just prop changes
Unlike `componentWillReceiveProps`, `getDerivedStateFromProps` is called before every render - including `setState` calls. Always compare against previous values stored in state.
```jsx
// WRONG - fires on every render, including setState triggers
static getDerivedStateFromProps(props, state) {
return { sortedItems: sort(props.items) }; // re-sorts on every setState!
}
// CORRECT - only updates when items reference changes
static getDerivedStateFromProps(props, state) {
if (props.items !== state.prevItems) {
return { sortedItems: sort(props.items), prevItems: props.items };
}
return null;
}
```
### Trap 2: It cannot access `this`
`getDerivedStateFromProps` is a static method. No `this.props`, no `this.state`, no instance methods.
```jsx
// WRONG - no this in static method
static getDerivedStateFromProps(props, state) {
return { value: this.computeValue(props) }; // ReferenceError
}
// CORRECT - pure function of props + state
static getDerivedStateFromProps(props, state) {
return { value: computeValue(props) }; // standalone function
}
```
### Trap 3: Don't use it for side effects
If you need to fetch when a prop changes - use `componentDidUpdate`. `getDerivedStateFromProps` must be pure.
### When getDerivedStateFromProps is actually the wrong tool
If you find yourself doing complex logic in `getDerivedStateFromProps`, consider whether the consuming component should receive pre-processed data as a prop instead. The pattern exists for narrow use cases, not general prop-to-state syncing.

View File

@@ -0,0 +1,151 @@
# componentWillUpdate Migration Reference
## The Core Decision
```
Does componentWillUpdate read the DOM (scroll, size, position, selection)?
YES → getSnapshotBeforeUpdate (paired with componentDidUpdate)
NO (side effects, request cancellation, etc.) → componentDidUpdate
```
---
## Case A - Reads DOM Before Re-render {#case-a}
The method captures a DOM measurement (scroll position, element size, cursor position) before React applies the next update, so it can be restored or adjusted after.
**Before:**
```jsx
class MessageList extends React.Component {
componentWillUpdate(nextProps) {
if (nextProps.messages.length > this.props.messages.length) {
this.savedScrollHeight = this.listRef.current.scrollHeight;
this.savedScrollTop = this.listRef.current.scrollTop;
}
}
componentDidUpdate(prevProps) {
if (prevProps.messages.length < this.props.messages.length) {
const scrollDelta = this.listRef.current.scrollHeight - this.savedScrollHeight;
this.listRef.current.scrollTop = this.savedScrollTop + scrollDelta;
}
}
}
```
**After - getSnapshotBeforeUpdate + componentDidUpdate:**
```jsx
class MessageList extends React.Component {
// Called right before DOM updates are applied - perfect timing to read DOM
getSnapshotBeforeUpdate(prevProps, prevState) {
if (prevProps.messages.length < this.props.messages.length) {
return {
scrollHeight: this.listRef.current.scrollHeight,
scrollTop: this.listRef.current.scrollTop,
};
}
return null; // Return null when snapshot is not needed
}
// Receives the snapshot as the third argument
componentDidUpdate(prevProps, prevState, snapshot) {
if (snapshot !== null) {
const scrollDelta = this.listRef.current.scrollHeight - snapshot.scrollHeight;
this.listRef.current.scrollTop = snapshot.scrollTop + scrollDelta;
}
}
}
```
**Why this is better than componentWillUpdate:** In React 18 concurrent mode, there can be a gap between when `componentWillUpdate` runs and when the DOM actually updates. DOM reads in `componentWillUpdate` may be stale. `getSnapshotBeforeUpdate` runs synchronously right before the DOM is committed - the reads are always accurate.
**The contract:**
- Return a value from `getSnapshotBeforeUpdate` → that value becomes `snapshot` in `componentDidUpdate`
- Return `null``snapshot` in `componentDidUpdate` is `null`
- Always check `if (snapshot !== null)` in `componentDidUpdate`
- `getSnapshotBeforeUpdate` MUST be paired with `componentDidUpdate`
---
## Case B - Side Effects Before Update {#case-b}
The method cancels an in-flight request, clears a timer, or runs some preparatory side effect when props or state are about to change.
**Before:**
```jsx
class SearchResults extends React.Component {
componentWillUpdate(nextProps) {
if (nextProps.query !== this.props.query) {
this.currentRequest?.cancel();
this.setState({ loading: true, results: [] });
}
}
}
```
**After - move to componentDidUpdate (run AFTER the update):**
```jsx
class SearchResults extends React.Component {
componentDidUpdate(prevProps) {
if (prevProps.query !== this.props.query) {
// Cancel the stale request
this.currentRequest?.cancel();
// Start the new request for the updated query
this.setState({ loading: true, results: [] });
this.currentRequest = searchAPI(this.props.query)
.then(results => this.setState({ results, loading: false }));
}
}
}
```
**Note:** The side effect now runs AFTER the render, not before. In most cases this is correct - you want to react to the state that's actually showing, not the state that was showing. If you truly need to run something synchronously BEFORE a render, reconsider the design - that usually indicates state that should be managed differently.
---
## Both Cases in One Component
If a component had both DOM-reading AND side effects in `componentWillUpdate`:
```jsx
// Before: does both
componentWillUpdate(nextProps) {
// DOM read
if (isExpanding(nextProps)) {
this.savedHeight = this.ref.current.offsetHeight;
}
// Side effect
if (nextProps.query !== this.props.query) {
this.request?.cancel();
}
}
```
After: split into both patterns:
```jsx
// DOM read → getSnapshotBeforeUpdate
getSnapshotBeforeUpdate(prevProps, prevState) {
if (isExpanding(this.props)) {
return { height: this.ref.current.offsetHeight };
}
return null;
}
// Side effect → componentDidUpdate
componentDidUpdate(prevProps, prevState, snapshot) {
// Handle snapshot if present
if (snapshot !== null) { /* ... */ }
// Handle side effect
if (prevProps.query !== this.props.query) {
this.request?.cancel();
this.startNewRequest();
}
}
```

View File

@@ -0,0 +1,40 @@
---
name: react18-string-refs
description: 'Provides exact migration patterns for React string refs (ref="name" + this.refs.name) to React.createRef() in class components. Use this skill whenever migrating string ref usage - including single element refs, multiple refs in a component, refs in lists, callback refs, and refs passed to child components. Always use this skill before writing any ref migration code - the multiple-refs-in-list pattern is particularly tricky and this skill prevents the most common mistakes. Use it for React 18.3.1 migration (string refs warn) and React 19 migration (string refs removed).'
---
# React 18 String Refs Migration
String refs (`ref="myInput"` + `this.refs.myInput`) were deprecated in React 16.3, warn in React 18.3.1, and are **removed in React 19**.
## Quick Pattern Map
| Pattern | Reference |
|---|---|
| Single ref on a DOM element | [→ patterns.md#single-ref](references/patterns.md#single-ref) |
| Multiple refs in one component | [→ patterns.md#multiple-refs](references/patterns.md#multiple-refs) |
| Refs in a list / dynamic refs | [→ patterns.md#list-refs](references/patterns.md#list-refs) |
| Callback refs (alternative approach) | [→ patterns.md#callback-refs](references/patterns.md#callback-refs) |
| Ref passed to a child component | [→ patterns.md#forwarded-refs](references/patterns.md#forwarded-refs) |
## Scan Command
```bash
# Find all string ref assignments in JSX
grep -rn 'ref="' src/ --include="*.js" --include="*.jsx" | grep -v "\.test\."
# Find all this.refs accessors
grep -rn "this\.refs\." src/ --include="*.js" --include="*.jsx" | grep -v "\.test\."
```
Both should be migrated together - find the `ref="name"` and the `this.refs.name` accesses for each component as a pair.
## The Migration Rule
Every string ref migrates to `React.createRef()`:
1. Add `refName = React.createRef();` as a class field (or in constructor)
2. Replace `ref="refName"``ref={this.refName}` in JSX
3. Replace `this.refs.refName``this.refName.current` everywhere
Read `references/patterns.md` for the full before/after for each case.

View File

@@ -0,0 +1,301 @@
# String Refs - All Migration Patterns
## Single Ref on a DOM Element {#single-ref}
The most common case - one ref to one DOM node.
```jsx
// Before:
class SearchBox extends React.Component {
handleSearch() {
const value = this.refs.searchInput.value;
this.props.onSearch(value);
}
focusInput() {
this.refs.searchInput.focus();
}
render() {
return (
<div>
<input ref="searchInput" type="text" placeholder="Search..." />
<button onClick={() => this.handleSearch()}>Search</button>
</div>
);
}
}
```
```jsx
// After:
class SearchBox extends React.Component {
searchInputRef = React.createRef();
handleSearch() {
const value = this.searchInputRef.current.value;
this.props.onSearch(value);
}
focusInput() {
this.searchInputRef.current.focus();
}
render() {
return (
<div>
<input ref={this.searchInputRef} type="text" placeholder="Search..." />
<button onClick={() => this.handleSearch()}>Search</button>
</div>
);
}
}
```
---
## Multiple Refs in One Component {#multiple-refs}
Each string ref becomes its own named `createRef()` field.
```jsx
// Before:
class LoginForm extends React.Component {
handleSubmit(e) {
e.preventDefault();
const email = this.refs.emailField.value;
const password = this.refs.passwordField.value;
this.props.onSubmit({ email, password });
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input ref="emailField" type="email" />
<input ref="passwordField" type="password" />
<button type="submit">Log in</button>
</form>
);
}
}
```
```jsx
// After:
class LoginForm extends React.Component {
emailFieldRef = React.createRef();
passwordFieldRef = React.createRef();
handleSubmit(e) {
e.preventDefault();
const email = this.emailFieldRef.current.value;
const password = this.passwordFieldRef.current.value;
this.props.onSubmit({ email, password });
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input ref={this.emailFieldRef} type="email" />
<input ref={this.passwordFieldRef} type="password" />
<button type="submit">Log in</button>
</form>
);
}
}
```
---
## Refs in a List / Dynamic Refs {#list-refs}
String refs in a map/loop - the most tricky case. Each item needs its own ref.
```jsx
// Before:
class TabPanel extends React.Component {
focusTab(index) {
this.refs[`tab_${index}`].focus();
}
render() {
return (
<div>
{this.props.tabs.map((tab, i) => (
<button key={tab.id} ref={`tab_${i}`}>
{tab.label}
</button>
))}
</div>
);
}
}
```
```jsx
// After - use a Map to store refs dynamically:
class TabPanel extends React.Component {
tabRefs = new Map();
getOrCreateRef(id) {
if (!this.tabRefs.has(id)) {
this.tabRefs.set(id, React.createRef());
}
return this.tabRefs.get(id);
}
focusTab(index) {
const tab = this.props.tabs[index];
this.tabRefs.get(tab.id)?.current?.focus();
}
render() {
return (
<div>
{this.props.tabs.map((tab) => (
<button key={tab.id} ref={this.getOrCreateRef(tab.id)}>
{tab.label}
</button>
))}
</div>
);
}
}
```
**Alternative - callback ref for lists (simpler):**
```jsx
class TabPanel extends React.Component {
tabRefs = {};
focusTab(index) {
this.tabRefs[index]?.focus();
}
render() {
return (
<div>
{this.props.tabs.map((tab, i) => (
<button
key={tab.id}
ref={el => { this.tabRefs[i] = el; }} // callback ref stores DOM node directly
>
{tab.label}
</button>
))}
</div>
);
}
}
// Note: callback refs store the DOM node directly (not wrapped in .current)
// this.tabRefs[i] is the element, not this.tabRefs[i].current
```
---
## Callback Refs (Alternative to createRef) {#callback-refs}
Callback refs are an alternative to `createRef()`. They're useful for lists (above) and when you need to run code when the ref attaches/detaches.
```jsx
// Callback ref syntax:
class MyComponent extends React.Component {
// Callback ref - called with the element when it mounts, null when it unmounts
setInputRef = (el) => {
this.inputEl = el; // stores the DOM node directly (no .current needed)
};
focusInput() {
this.inputEl?.focus(); // direct DOM node access
}
render() {
return <input ref={this.setInputRef} />;
}
}
```
**When to use callback refs vs createRef:**
- `createRef()` - for a fixed number of refs known at component definition time (most cases)
- Callback refs - for dynamic lists, when you need to react to attach/detach, or when the ref might change
**Important:** Inline callback refs (defined in render) re-create a new function on every render, which causes the ref to be called with `null` then the element on each render cycle. Use a bound method or class field arrow function instead:
```jsx
// AVOID - new function every render, causes ref flicker:
render() {
return <input ref={(el) => { this.inputEl = el; }} />; // inline - bad
}
// PREFER - stable reference:
setInputRef = (el) => { this.inputEl = el; }; // class field - good
render() {
return <input ref={this.setInputRef} />;
}
```
---
## Ref Passed to a Child Component {#forwarded-refs}
If a string ref was passed to a custom component (not a DOM element), the migration also requires updating the child.
```jsx
// Before:
class Parent extends React.Component {
handleClick() {
this.refs.myInput.focus(); // Parent accesses child's DOM node
}
render() {
return (
<div>
<MyInput ref="myInput" />
<button onClick={() => this.handleClick()}>Focus</button>
</div>
);
}
}
// MyInput.js (child - class component):
class MyInput extends React.Component {
render() {
return <input className="my-input" />;
}
}
```
```jsx
// After:
class Parent extends React.Component {
myInputRef = React.createRef();
handleClick() {
this.myInputRef.current.focus();
}
render() {
return (
<div>
{/* React 18: forwardRef needed. React 19: ref is a direct prop */}
<MyInput ref={this.myInputRef} />
<button onClick={() => this.handleClick()}>Focus</button>
</div>
);
}
}
// MyInput.js (React 18 - use forwardRef):
import { forwardRef } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
return <input ref={ref} className="my-input" />;
});
// MyInput.js (React 19 - ref as direct prop, no forwardRef):
function MyInput({ ref, ...props }) {
return <input ref={ref} className="my-input" />;
}
```
---

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