
React18 Lifecycle Patterns
- 745 installs
- 37.1k repo stars
- Updated July 28, 2026
- github/awesome-copilot
react18-lifecycle-patterns migrates componentWillMount, componentWillReceiveProps, and componentWillUpdate to safe React 18 equivalents.
About
React 18 Lifecycle Patterns routes each unsafe lifecycle method to the correct modern replacement based on semantic intent rather than name matching alone. componentWillMount moves initial state to constructor, side effects to componentDidMount, or prop-derived state to constructor with props. componentWillReceiveProps becomes componentDidUpdate for async side effects or getDerivedStateFromProps for pure prop-to-state derivation with trap warnings in references. componentWillUpdate maps DOM reads before paint to getSnapshotBeforeUpdate paired with componentDidUpdate, or effect cancellation to componentDidUpdate with previous prop comparison. The UNSAFE_ prefix rule rejects permanent suppression because it does not fix concurrent safety or prepare for React 19 removal.
- Decision tables route each unsafe method by semantic category, not name alone.
- componentWillMount: constructor for state, didMount for effects, constructor(props) for derivation.
- componentWillReceiveProps: didUpdate for async, getDerivedStateFromProps for pure state sync.
- componentWillUpdate: getSnapshotBeforeUpdate for DOM reads before update.
- UNSAFE_ prefix is temporary only; React 19 removes these methods entirely.
React18 Lifecycle Patterns by the numbers
- 745 all-time installs (skills.sh)
- +17 installs in the week ending Jul 17, 2026 (Skillselion tracking)
- Ranked #459 of 2,277 Frontend Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
react18-lifecycle-patterns capabilities & compatibility
- Capabilities
- semantic routing tables per unsafe lifecycle met · constructor versus didmount versus didupdate sel · getderivedstatefromprops versus didupdate distin · getsnapshotbeforeupdate dom read migration patte · unsafe_ prefix temporary hold policy with todo c
- Use cases
- refactoring · testing
What react18-lifecycle-patterns says it does
Wrong category = wrong migration.
React 19 will remove this. Migrate before React 19 upgrade.
npx skills add https://github.com/github/awesome-copilot --skill react18-lifecycle-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 745 |
|---|---|
| repo stars | ★ 37.1k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 28, 2026 |
| Repository | github/awesome-copilot ↗ |
Which modern lifecycle replaces an unsafe method when the old code mixes state, effects, and DOM reads?
Migrate unsafe class lifecycle methods componentWillMount, componentWillReceiveProps, and componentWillUpdate to React 18.3.1 compliant patterns.
Who is it for?
Class components with UNSAFE_ lifecycle warnings during React 18.3.1 upgrades.
Skip if: Skip for function components with hooks, string refs, or dependency compatibility checks.
When should I use this skill?
User migrates componentWillMount, componentWillReceiveProps, componentWillUpdate, or sees UNSAFE_ warnings.
What you get
Semantic decision tables pointing to constructor, didMount, didUpdate, getDerivedStateFromProps, or getSnapshotBeforeUpdate.
- refactored class component code
- lifecycle migration pattern reference
Files
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 |
| Runs a side effect (fetch, subscription, DOM) | Move to componentDidMount | → componentWillMount.md |
| Derives initial state from props | Move to constructor with props | → componentWillMount.md |
componentWillReceiveProps - what does it do?
| What it does | Correct migration | Reference |
|---|---|---|
| Async side effect triggered by prop change (fetch, cancel) | componentDidUpdate | → componentWillReceiveProps.md |
| Pure state derivation from new props (no side effects) | getDerivedStateFromProps | → componentWillReceiveProps.md |
componentWillUpdate - what does it do?
| What it does | Correct migration | Reference |
|---|---|---|
| Reads the DOM before update (scroll, size, position) | getSnapshotBeforeUpdate | → componentWillUpdate.md |
| Cancels requests / runs effects before update | componentDidUpdate with prev comparison | → componentWillUpdate.md |
---
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:
// 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 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:
class UserList extends React.Component {
componentWillMount() {
this.setState({ items: [], loading: false, page: 1 });
}
render() { ... }
}After - move to constructor:
class UserList extends React.Component {
constructor(props) {
super(props);
this.state = { items: [], loading: false, page: 1 };
}
render() { ... }
}If constructor already exists, merge the state:
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:
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:
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:
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:
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:
// 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:
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 Migration Reference
The Core Decision
Does componentWillReceiveProps trigger async work or side effects?
YES → componentDidUpdate
NO (pure state derivation only) → getDerivedStateFromPropsWhen 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:
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:
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):
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:
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:
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.
// 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.
// 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 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:
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:
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 becomessnapshotincomponentDidUpdate - Return
null→snapshotincomponentDidUpdateisnull - Always check
if (snapshot !== null)incomponentDidUpdate getSnapshotBeforeUpdateMUST be paired withcomponentDidUpdate
---
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:
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):
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:
// 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:
// 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();
}
}Related skills
How it compares
Pick react18-lifecycle-patterns for surgical class-component lifecycle migrations; use broader React upgrade skills when converting entire codebases from classes to hooks.
FAQ
Is UNSAFE_ prefix a valid permanent fix?
No. It suppresses warnings but does not fix concurrent issues and React 19 removes the methods anyway.
Prop change triggers fetch - which replacement?
Use componentDidUpdate for async side effects triggered by prop changes.
Need scroll position before DOM update?
Use getSnapshotBeforeUpdate for DOM reads, paired with componentDidUpdate for applying changes.