React Component Lifecycle Methods with Examples

React component lifecycle methods are the functions React calls at each stage of a component’s life: when it is created and added to the page (mounting), when its props or state change (updating), when it is removed (unmounting), and when a child throws an error. Class components define methods like componentDidMount, componentDidUpdate and componentWillUnmount; function components get the same behavior from useEffect and its cleanup function. This guide logs the real order of every lifecycle method, then maps each one to hooks.

Every example was built with React 19.3.0 and Vite 8.3.0 (npx vite build) and opened in Microsoft Edge; the buttons were clicked with real mouse events and the outputs are the browser console messages. Reference: Component (class lifecycle methods) and useEffect on react.dev.

The three lifecycle phases

PhaseWhenClass component methods (in order)Function component
MountingComponent is created and inserted into the DOMconstructor → getDerivedStateFromProps → render → componentDidMountBody runs, then useLayoutEffect, then useEffect
UpdatingNew props, setState or forceUpdategetDerivedStateFromProps → shouldComponentUpdate → render → getSnapshotBeforeUpdate → componentDidUpdateBody runs again; effects whose dependencies changed clean up and run again
UnmountingComponent is removedcomponentWillUnmountCleanup functions of all effects
Error handlingA child throws while renderinggetDerivedStateFromError, componentDidCatchNo hook; use a class error boundary

Class component lifecycle methods in action

This component logs every lifecycle method. The page mounts it, then the buttons change its name prop and remove it:

import { Component, useState } from "react";

// prints to the console and to the <pre> on the page
const lines = [];
const log = (msg) => {
  console.log(msg);
  lines.push(msg);
  const pre = document.getElementById("log");      // not in the DOM yet during the first render
  if (pre) pre.textContent = lines.join("\n");
};

class Profile extends Component {
  constructor(props) {
    super(props);
    this.state = { views: 0 };
    log("1 constructor");
  }
  static getDerivedStateFromProps(props, state) {
    log("2 getDerivedStateFromProps");
    return null;                                   // no state change
  }
  componentDidMount() {
    log("4 componentDidMount");                    // fetch data, subscribe, start timers
  }
  shouldComponentUpdate(nextProps, nextState) {
    log("shouldComponentUpdate");
    return true;                                   // false would skip this update
  }
  getSnapshotBeforeUpdate(prevProps, prevState) {
    log("getSnapshotBeforeUpdate");
    return null;
  }
  componentDidUpdate(prevProps) {
    log(`componentDidUpdate: ${prevProps.name} -> ${this.props.name}`);
  }
  componentWillUnmount() {
    log("componentWillUnmount");                   // clean up here
  }
  render() {
    log("3 render");
    return <h3>Profile of {this.props.name}</h3>;
  }
}

export default function App() {
  const [name, setName] = useState("Ava");
  const [show, setShow] = useState(true);
  return (
    <div>
      {show && <Profile name={name} />}
      <button id="rename" onClick={() => { log("--- update"); setName("Ben"); }}>Change the name prop</button>
      <button id="remove" onClick={() => { log("--- unmount"); setShow(false); }}>Remove Profile</button>
      <pre id="log" />
    </div>
  );
}

Console output after loading the page, clicking “Change the name prop” and “Remove Profile”:

1 constructor
2 getDerivedStateFromProps
3 render
4 componentDidMount
--- update
2 getDerivedStateFromProps
shouldComponentUpdate
3 render
getSnapshotBeforeUpdate
componentDidUpdate: Ava -> Ben
--- unmount
componentWillUnmount
Edge page logging React class lifecycle methods: constructor, getDerivedStateFromProps, render, componentDidMount, then the update and unmount methods
The real call order for mount, update and unmount.
  • componentDidMount: the component is in the DOM. Fetch data, add event listeners, start timers.
  • shouldComponentUpdate: return false to skip a render (or extend PureComponent).
  • getSnapshotBeforeUpdate: read the DOM (like a scroll position) just before React changes it; the return value is passed to componentDidUpdate.
  • componentDidUpdate(prevProps, prevState): react to changes; compare with the previous values before doing work.
  • componentWillUnmount: undo everything from componentDidMount.

The old componentWillMount, componentWillReceiveProps and componentWillUpdate are legacy and only work with the UNSAFE_ prefix; do not use them in new code.

Function component lifecycle with hooks

Function components have no lifecycle methods. Instead, useEffect runs after render, and the function it returns (the cleanup) runs before the effect runs again and when the component unmounts. The dependency array controls when:

import { useEffect, useLayoutEffect, useState } from "react";

// prints to the console and to the <pre> on the page
const lines = [];
const log = (msg) => {
  console.log(msg);
  lines.push(msg);
  const pre = document.getElementById("log");      // not in the DOM yet during the first render
  if (pre) pre.textContent = lines.join("\n");
};

function Profile({ name }) {
  log(`render (name=${name})`);

  useEffect(() => {
    log("effect []: mounted");                           // like componentDidMount
    return () => log("cleanup []: unmounted");           // like componentWillUnmount
  }, []);

  useEffect(() => {
    log(`effect [name]: ${name}`);                       // runs on mount and when name changes
    return () => log(`cleanup [name]: ${name}`);         // runs before the next effect and on unmount
  }, [name]);

  useLayoutEffect(() => {
    log("layout effect (before the browser paints)");
  });

  return <h3>Profile of {name}</h3>;
}

export default function App() {
  const [name, setName] = useState("Ava");
  const [show, setShow] = useState(true);
  return (
    <div>
      {show && <Profile name={name} />}
      <button id="rename" onClick={() => { log("--- update"); setName("Ben"); }}>Change the name prop</button>
      <button id="remove" onClick={() => { log("--- unmount"); setShow(false); }}>Remove Profile</button>
      <pre id="log" />
    </div>
  );
}

Console output after loading the page, clicking “Change the name prop” and “Remove Profile”:

render (name=Ava)
layout effect (before the browser paints)
effect []: mounted
effect [name]: Ava
--- update
render (name=Ben)
layout effect (before the browser paints)
cleanup [name]: Ava
effect [name]: Ben
--- unmount
cleanup []: unmounted
cleanup [name]: Ben
Edge page logging React function component lifecycle: render, layout effect, useEffect with empty and name dependencies, cleanups on update and unmount
Effects and cleanups on mount, update and unmount.
Class methodHook equivalent
componentDidMountuseEffect(() => { ... }, [])
componentDidUpdate (for a value)useEffect(() => { ... }, [value])
componentWillUnmountThe cleanup: useEffect(() => () => { ... }, [])
shouldComponentUpdateReact.memo(Component)
getSnapshotBeforeUpdateuseLayoutEffect (runs before paint)
componentDidCatchNo hook: keep a class error boundary

Clean up in the unmount phase

Anything started when the component mounts must be stopped when it unmounts, or it keeps running (and may try to update a component that no longer exists):

import { useEffect, useState } from "react";

function Stopwatch() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const id = setInterval(() => setSeconds((s) => s + 1), 1000);
    console.log("interval started", id);
    return () => {
      clearInterval(id);                              // without this, the timer keeps running after unmount
      console.log("interval cleared", id);
    };
  }, []);

  return <p>Running for {seconds} s</p>;
}

export default function App() {
  const [on, setOn] = useState(true);
  return (
    <div>
      {on && <Stopwatch />}
      <button id="toggle" onClick={() => setOn(!on)}>{on ? "Stop" : "Start"}</button>
    </div>
  );
}

Console output after about 2 seconds, Stop, and Start again:

interval started 1
interval cleared 1
interval started 2

Error boundaries: the error-handling lifecycle

If a component throws while rendering, React removes the whole tree unless a parent error boundary catches it. Only class components can be boundaries:

import { Component, useState } from "react";

class ErrorBoundary extends Component {
  state = { error: null };

  static getDerivedStateFromError(error) {
    return { error };                                  // render the fallback on the next render
  }
  componentDidCatch(error, info) {
    console.log("componentDidCatch:", error.message);  // log it to your error service
  }
  render() {
    if (this.state.error) return <p style={{ color: "crimson" }}>Something went wrong: {this.state.error.message}</p>;
    return this.props.children;
  }
}

function Cart({ items }) {
  if (!items) throw new Error("Cart data is missing");
  return <p>Cart: {items.length} items</p>;
}

export default function App() {
  const [items, setItems] = useState(["pen", "ink"]);
  return (
    <div>
      <ErrorBoundary>
        <Cart items={items} />
      </ErrorBoundary>
      <p>The rest of the page still works.</p>
      <button id="break" onClick={() => setItems(null)}>Break the cart</button>
    </div>
  );
}

Console output after clicking “Break the cart” (stack trace lines removed):

[error] Error: Cart data is missing
componentDidCatch: Cart data is missing
Edge page where a React error boundary shows Something went wrong: Cart data is missing while the rest of the page keeps working
The boundary replaces only the broken part of the page.

After the click, componentDidCatch logs Cart data is missing to the console and React also reports the caught error with console.error.

Why effects run twice in development

In development, <StrictMode> mounts every component, unmounts it and mounts it again, to check that your cleanup undoes your effect. This example, built once in development mode and once for production:

import { useEffect } from "react";

export default function App() {
  useEffect(() => {
    console.log("effect: connect");
    return () => console.log("cleanup: disconnect");
  }, []);
  return <p>Open the console</p>;
}

Development build:

effect: connect
cleanup: disconnect
effect: connect

Production build:

effect: connect

The extra cleanup and effect only happen in development. If the double run causes a bug, the effect is missing a cleanup.

Keep going with these React guides:

Frequently asked questions

What are the React component lifecycle methods?

Mounting: constructor, getDerivedStateFromProps, render, componentDidMount. Updating: shouldComponentUpdate, render, getSnapshotBeforeUpdate, componentDidUpdate. Unmounting: componentWillUnmount.

What are the phases of the React component lifecycle?

Mounting, updating and unmounting, plus error handling when a child component throws.

Do function components have lifecycle methods?

No. They use hooks: useEffect with an empty array for mount, dependencies for updates, and the cleanup function for unmount.

What is the useEffect equivalent of componentDidMount?

useEffect(() => { /* code */ }, []) runs once after the first render.

Why does my useEffect run twice?

React StrictMode runs mount, cleanup, mount in development to test your cleanup. It runs once in production.

Which lifecycle method should I use to fetch data?

componentDidMount in classes or useEffect in function components (or a data library such as React Query).

Leave a Comment