Installation
gh skills-hub install react18-lifecycle-patterns Don't have the extension? Run gh extension install samueltauil/skills-hub first.
Download and extract to your repository:
.github/skills/react18-lifecycle-patterns/ Extract the ZIP to .github/skills/ in your repo. The folder name must match react18-lifecycle-patterns for Copilot to auto-discover it.
Skill Files (4)
SKILL.md 3.6 KB
---
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.
componentWillMount.md 3.7 KB
# 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 }));
}
```
componentWillReceiveProps.md 5.0 KB
# 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.
componentWillUpdate.md 4.7 KB
# 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();
}
}
```
License (MIT)
View full license text
MIT License Copyright GitHub, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.