React Lifecycle

Understand React component lifecycle phases and map class methods to hooks.

TL;DR

  1. 01Every component goes through mount, update, and unmount phases.
  2. 02Class lifecycle methods map onto specific hooks and timing rules.
  3. 03React skips re-renders when memoization or keys say state is unchanged.

Tips

  1. 01Map each class lifecycle method to its hooks equivalent one at a time when migrating, rather than rewriting a whole component at once.
  2. 02Use React.memo or PureComponent to skip unnecessary re-renders instead of fighting them with manual shouldComponentUpdate logic.

Warnings

  1. 01Calling setState synchronously inside componentDidMount or its hook equivalent forces React to render twice before the browser paints anything.
  2. 02Updating state after a component unmounts logs a warning in class components and can cause stale closures in hooks-based equivalents.

Component Lifecycle Phases

  • Mount: component is created and inserted into the DOM.
  • Update: component re-renders due to state or prop changes.
  • Unmount: component is removed from the DOM.
    function Component() {
      // Mount: run once
      useEffect(() => {
        console.log("Mounted");
      }, []);
      
      // Unmount: cleanup
      useEffect(() => {
        return () => console.log("Unmounting");
      }, []);
    }
  • React 18 Strict Mode mounts, unmounts, then remounts in development.
    // In development with Strict Mode, lifecycle methods run twice
    // This is intentional — it proves teardown and setup are symmetric
  • The same component instance updates without dismounting on re-renders.
    // Changing props or state updates the component
    // — it does NOT unmount and remount
  • A changed key prop forces React to unmount the old instance and mount a fresh one.
    // Changing the key remounts the component from scratch,
    // resetting all of its internal state
    <UserPanel key={userId} />

Class Lifecycle Methods

  • componentDidMount fires once after the component is first inserted into the DOM.
    class Profile extends React.Component {
      componentDidMount() {
        console.log("Mounted, safe to fetch or measure DOM");
      }
    }
  • componentDidUpdate fires after every re-render except the first, receiving previous props and state.
    componentDidUpdate(prevProps, prevState) {
      if (prevProps.userId !== this.props.userId) {
        this.loadUser(this.props.userId);
      }
    }
  • componentWillUnmount fires once, right before React removes the component from the DOM.
    componentWillUnmount() {
      this.subscription.unsubscribe();
    }
  • shouldComponentUpdate runs before re-rendering and can return false to skip the render entirely.
    shouldComponentUpdate(nextProps) {
      // Skip re-render if the displayed value hasn't changed
      return nextProps.value !== this.props.value;
    }
  • getDerivedStateFromProps runs before every render to sync state from incoming props.
    static getDerivedStateFromProps(props, state) {
      if (props.id !== state.prevId) {
        return { selected: null, prevId: props.id };
      }
      return null;
    }

Class to Hooks Mapping

  • componentDidMount maps to useEffect with an empty dependency array, since both run once after the initial render.
    // Class: componentDidMount() { ... }
    useEffect(() => {
      // runs once, after mount
    }, []);
  • componentDidUpdate maps to useEffect with specific dependencies, since both run after updates to those values.
    // Class: componentDidUpdate(prevProps) { if (prevProps.id !== this.props.id) ... }
    useEffect(() => {
      // runs after mount AND after every id change
    }, [id]);
  • componentWillUnmount maps to the function returned from useEffect.
    // Class: componentWillUnmount() { this.sub.unsubscribe(); }
    useEffect(() => {
      const sub = subscribe();
      return () => sub.unsubscribe();
    }, []);
  • shouldComponentUpdate maps to wrapping the function component in React.memo.
    // Class: extends React.PureComponent, or custom shouldComponentUpdate
    const Row = React.memo((props) => {
      return <tr>{props.label}</tr>;
    });
  • getDerivedStateFromProps usually maps to deriving the value directly during render instead of storing it in state.
    // Class: static getDerivedStateFromProps synced state.prevId to props.id
    const [prevId, setPrevId] = useState(id);
    if (id !== prevId) {
      setPrevId(id);
      setSelected(null); // adjust state during render, not in an effect
    }

Reconciliation and Re-Renders

  • A parent re-rendering re-renders every child by default, even children whose props are unchanged.
    // Parent state change re-renders Child even if childProp never changes
    function Parent() {
      const [count, setCount] = useState(0);
      return <Child childProp="static" />;
    }
  • React.memo skips re-rendering a function component when its props are shallowly equal to the last render.
    const Child = React.memo(({ childProp }) => {
      return <div>{childProp}</div>;
    }); // re-renders only when childProp's reference changes
  • PureComponent is the class equivalent of React.memo, shallow-comparing props and state automatically.
    class Row extends React.PureComponent {
      // Automatically skips render if props/state are shallowly equal
      render() { return <tr>{this.props.label}</tr>; }
    }
  • Changing a list item's key forces React to discard the old DOM node and lifecycle state instead of updating it in place.
    // Using array index as key can cause React to reuse the wrong instance
    // when items are reordered — prefer a stable id
    items.map((item) => <Row key={item.id} item={item} />)
  • useMemo and useCallback stop new object or function references from defeating memoization on the next render.
    // Without useCallback, onSave is a new function every render,
    // which breaks React.memo on Child
    const onSave = useCallback(() => save(id), [id]);

Common Lifecycle Bugs

  • Calling setState synchronously inside componentDidMount triggers an extra render before the browser paints.
    componentDidMount() {
      // Causes a second render right after the first — avoid when possible
      this.setState({ ready: true });
    }
  • Updating state in componentWillUnmount or after an effect's cleanup has run logs a no-op warning.
    componentWillUnmount() {
      // Warning: Can't perform a React state update on an unmounted component
      this.setState({ closed: true }); // remove this call instead
    }
  • Child lifecycle methods fire before the parent's during mount, but the parent's fire first during unmount.
    // Mount order: Child.componentDidMount, then Parent.componentDidMount
    // Unmount order: Parent.componentWillUnmount, then Child.componentWillUnmount
  • Calling shouldComponentUpdate but forgetting to compare nested fields silently blocks needed re-renders.
    shouldComponentUpdate(nextProps) {
      // Bug: compares the array reference, not its contents
      return nextProps.items !== this.props.items;
    }
  • Mixing getDerivedStateFromProps with side effects like fetching breaks its contract — it must stay pure.
    static getDerivedStateFromProps(props) {
      // Wrong: side effects don't belong here, this method must be pure
      // fetch(props.url) — move data fetching to componentDidMount/useEffect instead
      return null;
    }

FAQ