mirror of
https://github.com/github/awesome-copilot.git
synced 2026-04-13 03:35:55 +00:00
feat: Adds React 18 and 19 migration plugin (#1339)
- Adds React 18 and 19 migration orchestration plugins - Introduces comprehensive upgrade toolkits for migrating legacy React 16/17 and 18 codebases to React 18.3.1 and 19, respectively. Each plugin bundles specialized agents and skills for exhaustive audit, dependency management, class/component API migration, test suite transformation, and batching regression fixes. - The React 18 toolkit targets class-component-heavy apps, ensures safe lifecycle and context transitions, resolves dependency blockers, and fully automates test migrations including Enzyme removal. The React 19 toolkit addresses breaking changes such as removal of legacy APIs, defaultProps on function components, and forwardRef, while enforcing a gated, memory-resumable migration pipeline. - Both plugins update documentation, plugin registries, and skill references to support reliable, repeatable enterprise-scale React migrations.
This commit is contained in:
committed by
GitHub
parent
f4909cd581
commit
7f7b1b9b46
@@ -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 }));
|
||||
}
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user