chore: publish from staged

This commit is contained in:
github-actions[bot]
2026-04-13 01:02:36 +00:00
parent e37cd3123f
commit 2f4953242f
467 changed files with 97528 additions and 276 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" />;
}
```
---