Files
awesome-copilot/skills/react18-lifecycle-patterns/references/componentWillUpdate.md
Saravanan Rajaraman 7f7b1b9b46 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.
2026-04-09 15:18:52 +10:00

4.7 KiB

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

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 becomes snapshot in componentDidUpdate
  • Return nullsnapshot in componentDidUpdate is null
  • Always check if (snapshot !== null) in componentDidUpdate
  • getSnapshotBeforeUpdate MUST be paired with componentDidUpdate

Case B - Side Effects Before Update

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();
  }
}