{"kind":"Skill","metadata":{"namespace":"community","name":"react18-lifecycle-patterns","version":"0.1.0"},"spec":{"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.","files":{"SKILL.md":"---\nname: react18-lifecycle-patterns\ndescription: '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.'\n---\n\n# React 18 Lifecycle Patterns\n\nReference for migrating the three unsafe class component lifecycle methods to React 18.3.1 compliant patterns.\n\n## Quick Decision Guide\n\nBefore 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.\n\n### componentWillMount - what does it do?\n\n| What it does | Correct migration | Reference |\n|---|---|---|\n| Sets initial state (`this.setState(...)`) | Move to `constructor` | [→ componentWillMount.md](references/componentWillMount.md#case-a) |\n| Runs a side effect (fetch, subscription, DOM) | Move to `componentDidMount` | [→ componentWillMount.md](references/componentWillMount.md#case-b) |\n| Derives initial state from props | Move to `constructor` with props | [→ componentWillMount.md](references/componentWillMount.md#case-c) |\n\n### componentWillReceiveProps - what does it do?\n\n| What it does | Correct migration | Reference |\n|---|---|---|\n| Async side effect triggered by prop change (fetch, cancel) | `componentDidUpdate` | [→ componentWillReceiveProps.md](references/componentWillReceiveProps.md#case-a) |\n| Pure state derivation from new props (no side effects) | `getDerivedStateFromProps` | [→ componentWillReceiveProps.md](references/componentWillReceiveProps.md#case-b) |\n\n### componentWillUpdate - what does it do?\n\n| What it does | Correct migration | Reference |\n|---|---|---|\n| Reads the DOM before update (scroll, size, position) | `getSnapshotBeforeUpdate` | [→ componentWillUpdate.md](references/componentWillUpdate.md#case-a) |\n| Cancels requests / runs effects before update | `componentDidUpdate` with prev comparison | [→ componentWillUpdate.md](references/componentWillUpdate.md#case-b) |\n\n---\n\n## The UNSAFE_ Prefix Rule\n\n**Never use `UNSAFE_componentWillMount`, `UNSAFE_componentWillReceiveProps`, or `UNSAFE_componentWillUpdate` as a permanent fix.**\n\nPrefixing suppresses the React 18.3.1 warning but does NOT:\n- Fix concurrent mode safety issues\n- Prepare the codebase for React 19 (where these are removed, with or without the prefix)\n- Fix the underlying semantic problem the migration is meant to address\n\nThe UNSAFE_ prefix is only appropriate as a temporary hold while scheduling the real migration sprint. Mark any UNSAFE_ prefix additions with:\n```jsx\n// TODO: React 19 will remove this. Migrate before React 19 upgrade.\n// UNSAFE_ prefix added temporarily - replace with componentDidMount / getDerivedStateFromProps / etc.\n```\n\n---\n\n## Reference Files\n\nRead the full reference file for the lifecycle method you are migrating:\n\n- **`references/componentWillMount.md`** - 3 cases with full before/after code\n- **`references/componentWillReceiveProps.md`** - getDerivedStateFromProps trap warnings, full examples\n- **`references/componentWillUpdate.md`** - getSnapshotBeforeUpdate + componentDidUpdate pairing\n\nRead the relevant file before writing any migration code.\n","references/componentWillMount.md":"# componentWillMount Migration Reference\n\n## Case A - Initializes State {#case-a}\n\nThe method only calls `this.setState()` with static or computed values that do not depend on async operations.\n\n**Before:**\n\n```jsx\nclass UserList extends React.Component {\n  componentWillMount() {\n    this.setState({ items: [], loading: false, page: 1 });\n  }\n  render() { ... }\n}\n```\n\n**After - move to constructor:**\n\n```jsx\nclass UserList extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = { items: [], loading: false, page: 1 };\n  }\n  render() { ... }\n}\n```\n\n**If constructor already exists**, merge the state:\n\n```jsx\nclass UserList extends React.Component {\n  constructor(props) {\n    super(props);\n    // Existing state merged with componentWillMount state:\n    this.state = {\n      ...this.existingState,  // whatever was already here\n      items: [],\n      loading: false,\n      page: 1,\n    };\n  }\n}\n```\n\n---\n\n## Case B - Runs a Side Effect {#case-b}\n\nThe method fetches data, sets up subscriptions, interacts with external APIs, or touches the DOM.\n\n**Before:**\n\n```jsx\nclass UserDashboard extends React.Component {\n  componentWillMount() {\n    this.subscription = this.props.eventBus.subscribe(this.handleEvent);\n    fetch(`/api/users/${this.props.userId}`)\n      .then(r =\u003e r.json())\n      .then(user =\u003e this.setState({ user, loading: false }));\n    this.setState({ loading: true });\n  }\n}\n```\n\n**After - move to componentDidMount:**\n\n```jsx\nclass UserDashboard extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = { loading: true, user: null }; // initial state here\n  }\n\n  componentDidMount() {\n    // All side effects move here - runs after first render\n    this.subscription = this.props.eventBus.subscribe(this.handleEvent);\n    fetch(`/api/users/${this.props.userId}`)\n      .then(r =\u003e r.json())\n      .then(user =\u003e this.setState({ user, loading: false }));\n  }\n\n  componentWillUnmount() {\n    // Always pair subscriptions with cleanup\n    this.subscription?.unsubscribe();\n  }\n}\n```\n\n**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.\n\n---\n\n## Case C - Derives Initial State from Props {#case-c}\n\nThe method reads `this.props` to compute an initial state value.\n\n**Before:**\n\n```jsx\nclass PriceDisplay extends React.Component {\n  componentWillMount() {\n    this.setState({\n      formattedPrice: `$${this.props.price.toFixed(2)}`,\n      isDiscount: this.props.price \u003c this.props.originalPrice,\n    });\n  }\n}\n```\n\n**After - constructor with props:**\n\n```jsx\nclass PriceDisplay extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = {\n      formattedPrice: `$${props.price.toFixed(2)}`,\n      isDiscount: props.price \u003c props.originalPrice,\n    };\n  }\n}\n```\n\n**Note:** If this initial state needs to UPDATE when props change later, that's a `getDerivedStateFromProps` case - see `componentWillReceiveProps.md` Case B.\n\n---\n\n## Multiple Patterns in One Method\n\nIf a single `componentWillMount` does both state init AND side effects:\n\n```jsx\n// Mixed - state init + fetch\ncomponentWillMount() {\n  this.setState({ loading: true, items: [] });              // Case A\n  fetch('/api/items').then(r =\u003e r.json())                   // Case B\n    .then(items =\u003e this.setState({ items, loading: false }));\n}\n```\n\nSplit them:\n\n```jsx\nconstructor(props) {\n  super(props);\n  this.state = { loading: true, items: [] }; // Case A → constructor\n}\n\ncomponentDidMount() {\n  fetch('/api/items').then(r =\u003e r.json())    // Case B → componentDidMount\n    .then(items =\u003e this.setState({ items, loading: false }));\n}\n```\n","references/componentWillReceiveProps.md":"# componentWillReceiveProps Migration Reference\n\n## The Core Decision\n\n```\nDoes componentWillReceiveProps trigger async work or side effects?\n  YES → componentDidUpdate\n  NO (pure state derivation only) → getDerivedStateFromProps\n```\n\nWhen in doubt: use `componentDidUpdate`. It's always safe.\n`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.\n\n---\n\n## Case A - Async Side Effects / Fetch on Prop Change {#case-a}\n\nThe method fetches data, cancels requests, updates external state, or runs any async operation when a prop changes.\n\n**Before:**\n\n```jsx\nclass UserProfile extends React.Component {\n  componentWillReceiveProps(nextProps) {\n    if (nextProps.userId !== this.props.userId) {\n      this.setState({ loading: true, profile: null });\n      fetchProfile(nextProps.userId)\n        .then(profile =\u003e this.setState({ profile, loading: false }))\n        .catch(err =\u003e this.setState({ error: err, loading: false }));\n    }\n  }\n}\n```\n\n**After - componentDidUpdate:**\n\n```jsx\nclass UserProfile extends React.Component {\n  componentDidUpdate(prevProps) {\n    if (prevProps.userId !== this.props.userId) {\n      // Use this.props (not nextProps - the update already happened)\n      this.setState({ loading: true, profile: null });\n      fetchProfile(this.props.userId)\n        .then(profile =\u003e this.setState({ profile, loading: false }))\n        .catch(err =\u003e this.setState({ error: err, loading: false }));\n    }\n  }\n}\n```\n\n**Key difference:** `componentDidUpdate` receives `prevProps` - you compare `prevProps.x !== this.props.x` instead of `this.props.x !== nextProps.x`. The update has already applied.\n\n**Cancellation pattern** (important for async):\n\n```jsx\nclass UserProfile extends React.Component {\n  _requestId = 0;\n\n  componentDidUpdate(prevProps) {\n    if (prevProps.userId !== this.props.userId) {\n      const requestId = ++this._requestId;\n      this.setState({ loading: true });\n      fetchProfile(this.props.userId).then(profile =\u003e {\n        // Ignore stale responses if userId changed again\n        if (requestId === this._requestId) {\n          this.setState({ profile, loading: false });\n        }\n      });\n    }\n  }\n}\n```\n\n---\n\n## Case B - Pure State Derivation from Props {#case-b}\n\nThe method only derives state values from the new props synchronously. No async work, no side effects, no external calls.\n\n**Before:**\n\n```jsx\nclass SortedList extends React.Component {\n  componentWillReceiveProps(nextProps) {\n    if (nextProps.items !== this.props.items) {\n      this.setState({\n        sortedItems: [...nextProps.items].sort((a, b) =\u003e a.name.localeCompare(b.name)),\n      });\n    }\n  }\n}\n```\n\n**After - getDerivedStateFromProps:**\n\n```jsx\nclass SortedList extends React.Component {\n  // Must track previous prop to detect changes\n  static getDerivedStateFromProps(props, state) {\n    if (props.items !== state.prevItems) {\n      return {\n        sortedItems: [...props.items].sort((a, b) =\u003e a.name.localeCompare(b.name)),\n        prevItems: props.items, // ← always store the prop you're comparing\n      };\n    }\n    return null; // null = no state change\n  }\n\n  constructor(props) {\n    super(props);\n    this.state = {\n      sortedItems: [...props.items].sort((a, b) =\u003e a.name.localeCompare(b.name)),\n      prevItems: props.items, // ← initialize in constructor too\n    };\n  }\n}\n```\n\n---\n\n## getDerivedStateFromProps - Traps and Warnings\n\n### Trap 1: It fires on EVERY render, not just prop changes\n\nUnlike `componentWillReceiveProps`, `getDerivedStateFromProps` is called before every render - including `setState` calls. Always compare against previous values stored in state.\n\n```jsx\n// WRONG - fires on every render, including setState triggers\nstatic getDerivedStateFromProps(props, state) {\n  return { sortedItems: sort(props.items) }; // re-sorts on every setState!\n}\n\n// CORRECT - only updates when items reference changes\nstatic getDerivedStateFromProps(props, state) {\n  if (props.items !== state.prevItems) {\n    return { sortedItems: sort(props.items), prevItems: props.items };\n  }\n  return null;\n}\n```\n\n### Trap 2: It cannot access `this`\n\n`getDerivedStateFromProps` is a static method. No `this.props`, no `this.state`, no instance methods.\n\n```jsx\n// WRONG - no this in static method\nstatic getDerivedStateFromProps(props, state) {\n  return { value: this.computeValue(props) }; // ReferenceError\n}\n\n// CORRECT - pure function of props + state\nstatic getDerivedStateFromProps(props, state) {\n  return { value: computeValue(props) }; // standalone function\n}\n```\n\n### Trap 3: Don't use it for side effects\n\nIf you need to fetch when a prop changes - use `componentDidUpdate`. `getDerivedStateFromProps` must be pure.\n\n### When getDerivedStateFromProps is actually the wrong tool\n\nIf 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.\n","references/componentWillUpdate.md":"# componentWillUpdate Migration Reference\n\n## The Core Decision\n\n```\nDoes componentWillUpdate read the DOM (scroll, size, position, selection)?\n  YES → getSnapshotBeforeUpdate (paired with componentDidUpdate)\n  NO (side effects, request cancellation, etc.) → componentDidUpdate\n```\n\n---\n\n## Case A - Reads DOM Before Re-render {#case-a}\n\nThe 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.\n\n**Before:**\n\n```jsx\nclass MessageList extends React.Component {\n  componentWillUpdate(nextProps) {\n    if (nextProps.messages.length \u003e this.props.messages.length) {\n      this.savedScrollHeight = this.listRef.current.scrollHeight;\n      this.savedScrollTop = this.listRef.current.scrollTop;\n    }\n  }\n\n  componentDidUpdate(prevProps) {\n    if (prevProps.messages.length \u003c this.props.messages.length) {\n      const scrollDelta = this.listRef.current.scrollHeight - this.savedScrollHeight;\n      this.listRef.current.scrollTop = this.savedScrollTop + scrollDelta;\n    }\n  }\n}\n```\n\n**After - getSnapshotBeforeUpdate + componentDidUpdate:**\n\n```jsx\nclass MessageList extends React.Component {\n  // Called right before DOM updates are applied - perfect timing to read DOM\n  getSnapshotBeforeUpdate(prevProps, prevState) {\n    if (prevProps.messages.length \u003c this.props.messages.length) {\n      return {\n        scrollHeight: this.listRef.current.scrollHeight,\n        scrollTop: this.listRef.current.scrollTop,\n      };\n    }\n    return null; // Return null when snapshot is not needed\n  }\n\n  // Receives the snapshot as the third argument\n  componentDidUpdate(prevProps, prevState, snapshot) {\n    if (snapshot !== null) {\n      const scrollDelta = this.listRef.current.scrollHeight - snapshot.scrollHeight;\n      this.listRef.current.scrollTop = snapshot.scrollTop + scrollDelta;\n    }\n  }\n}\n```\n\n**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.\n\n**The contract:**\n\n- Return a value from `getSnapshotBeforeUpdate` → that value becomes `snapshot` in `componentDidUpdate`\n- Return `null` → `snapshot` in `componentDidUpdate` is `null`\n- Always check `if (snapshot !== null)` in `componentDidUpdate`\n- `getSnapshotBeforeUpdate` MUST be paired with `componentDidUpdate`\n\n---\n\n## Case B - Side Effects Before Update {#case-b}\n\nThe method cancels an in-flight request, clears a timer, or runs some preparatory side effect when props or state are about to change.\n\n**Before:**\n\n```jsx\nclass SearchResults extends React.Component {\n  componentWillUpdate(nextProps) {\n    if (nextProps.query !== this.props.query) {\n      this.currentRequest?.cancel();\n      this.setState({ loading: true, results: [] });\n    }\n  }\n}\n```\n\n**After - move to componentDidUpdate (run AFTER the update):**\n\n```jsx\nclass SearchResults extends React.Component {\n  componentDidUpdate(prevProps) {\n    if (prevProps.query !== this.props.query) {\n      // Cancel the stale request\n      this.currentRequest?.cancel();\n      // Start the new request for the updated query\n      this.setState({ loading: true, results: [] });\n      this.currentRequest = searchAPI(this.props.query)\n        .then(results =\u003e this.setState({ results, loading: false }));\n    }\n  }\n}\n```\n\n**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.\n\n---\n\n## Both Cases in One Component\n\nIf a component had both DOM-reading AND side effects in `componentWillUpdate`:\n\n```jsx\n// Before: does both\ncomponentWillUpdate(nextProps) {\n  // DOM read\n  if (isExpanding(nextProps)) {\n    this.savedHeight = this.ref.current.offsetHeight;\n  }\n  // Side effect\n  if (nextProps.query !== this.props.query) {\n    this.request?.cancel();\n  }\n}\n```\n\nAfter: split into both patterns:\n\n```jsx\n// DOM read → getSnapshotBeforeUpdate\ngetSnapshotBeforeUpdate(prevProps, prevState) {\n  if (isExpanding(this.props)) {\n    return { height: this.ref.current.offsetHeight };\n  }\n  return null;\n}\n\n// Side effect → componentDidUpdate\ncomponentDidUpdate(prevProps, prevState, snapshot) {\n  // Handle snapshot if present\n  if (snapshot !== null) { /* ... */ }\n\n  // Handle side effect\n  if (prevProps.query !== this.props.query) {\n    this.request?.cancel();\n    this.startNewRequest();\n  }\n}\n```\n"},"import":{"commit_sha":"541b7819d8c3545c6df122491af4fa1eae415779","imported_at":"2026-05-18T20:05:35Z","license_text":"MIT License\n\nCopyright GitHub, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.","owner":"github","repo":"github/awesome-copilot","source_url":"https://github.com/github/awesome-copilot/tree/541b7819d8c3545c6df122491af4fa1eae415779/plugins/react18-upgrade/skills/react18-lifecycle-patterns"}},"content_hash":[142,187,44,38,14,211,154,114,121,244,224,185,81,133,145,146,42,169,234,202,188,151,206,216,192,252,188,181,193,199,92,97],"trust_level":"unsigned","yanked":false}
