Files
2026-04-10 04:45:41 +00: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();
  }
}