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
| Phase | When | Class component methods (in order) | Function component |
|---|---|---|---|
| Mounting | Component is created and inserted into the DOM | constructor → getDerivedStateFromProps → render → componentDidMount | Body runs, then useLayoutEffect, then useEffect |
| Updating | New props, setState or forceUpdate | getDerivedStateFromProps → shouldComponentUpdate → render → getSnapshotBeforeUpdate → componentDidUpdate | Body runs again; effects whose dependencies changed clean up and run again |
| Unmounting | Component is removed | componentWillUnmount | Cleanup functions of all effects |
| Error handling | A child throws while rendering | getDerivedStateFromError, componentDidCatch | No 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
- componentDidMount: the component is in the DOM. Fetch data, add event listeners, start timers.
- shouldComponentUpdate: return
falseto skip a render (or extendPureComponent). - 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
| Class method | Hook equivalent |
|---|---|
componentDidMount | useEffect(() => { ... }, []) |
componentDidUpdate (for a value) | useEffect(() => { ... }, [value]) |
componentWillUnmount | The cleanup: useEffect(() => () => { ... }, []) |
shouldComponentUpdate | React.memo(Component) |
getSnapshotBeforeUpdate | useLayoutEffect (runs before paint) |
componentDidCatch | No 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
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:
- React class-based components
- Use React hooks in class components
- Error boundaries for functional components
- Force a React component to re-render
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).
Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn