Component Lifecycle
Learn the three phases every React component goes through — mounting, updating, and unmounting — and how they map onto Hooks in function components.
Introduction
Every component in a React application is born, exists on the screen for a while reacting to changes, and eventually disappears. React calls these three stages the component lifecycle, and understanding them is essential for knowing exactly when your code runs — when to fetch data, when to set up a subscription, and when to clean it up before the component disappears.
In modern React, function components express all three lifecycle phases through Hooks, primarily useEffect. This lesson walks through each phase individually, shows how useEffect maps onto them, and — because you will likely encounter older codebases — compares this to the class-based lifecycle methods React used before Hooks existed.
- The three phases every component goes through: mounting, updating, and unmounting.
- How useEffect(fn, []) maps to the mounting phase.
- How useEffect(fn, [dep]) maps to the updating phase.
- How the cleanup function returned from useEffect maps to unmounting.
- How this compares to the older componentDidMount / componentDidUpdate / componentWillUnmount methods.
The Three Lifecycle Phases
Regardless of whether a component is written as a function or a class, it always passes through the same three conceptual stages during its life on the screen.
Mounting
The component is created and inserted into the DOM for the first time.
Updating
The component re-renders because its props or state changed.
Unmounting
The component is removed from the DOM and cleaned up.
Mounting
Mounting happens exactly once: the first time a component appears on the screen. This is the ideal moment to run one-time setup code, like fetching initial data or starting a subscription. In a function component, an empty dependency array tells useEffect to run its function only after this first render.
import { useEffect, useState } from "react";
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
console.log("Component mounted — fetching user data");
fetchUser(userId).then(setUser);
}, []); // empty array = runs once, after the first render
return <div>{user ? user.name : "Loading..."}</div>;
}Component mounted — fetching user data (logged exactly once)Updating
Updating happens every time a component re-renders because its props or state changed. If you include a value in useEffect's dependency array, the effect re-runs whenever that specific value changes — this is how you react to updates without re-running setup logic on every single render.
useEffect(() => {
console.log("userId changed — re-fetching user data");
fetchUser(userId).then(setUser);
}, [userId]); // reruns whenever userId changesuserId changed — re-fetching user data
(logged again each time the userId prop changes to a new value)The same useEffect Hook actually covers both mounting and updating — it always runs after the first render (mount) and then again after any render where a listed dependency changed (update). The dependency array is what distinguishes the two behaviors.
Unmounting
Unmounting happens when a component is removed from the screen — for example, when the user navigates away from the page it lives on. This is the moment to clean up anything that would otherwise keep running or leak memory, like a timer, a WebSocket connection, or an event listener. In a function component, this cleanup is handled by the function you optionally return from inside useEffect.
useEffect(() => {
const timer = setInterval(() => {
console.log("tick");
}, 1000);
// This cleanup function runs when the component unmounts
return () => {
console.log("Component unmounting — clearing timer");
clearInterval(timer);
};
}, []);tick
tick
tick
Component unmounting — clearing timer (logged when the component is removed)The Full Picture
Putting all three phases together in one component makes the pattern clear: setup happens on mount, react-to-changes logic happens on update, and cleanup happens on unmount — all expressed through the same useEffect Hook.
useEffect(() => {
console.log("Mounted or userId changed — subscribing");
const subscription = subscribeToUser(userId, setUser);
return () => {
console.log("Cleaning up before next effect or unmount");
subscription.unsubscribe();
};
}, [userId]);Comparison to Class Lifecycle Methods
Before Hooks existed, this same behavior was expressed using named lifecycle methods on class components. You may still encounter these in legacy codebases, so it helps to know how they map onto the Hooks you just learned.
| Phase | Class Component Method | Function Component Equivalent |
|---|---|---|
| Mounting | componentDidMount() | useEffect(fn, []) |
| Updating | componentDidUpdate(prevProps, prevState) | useEffect(fn, [dep]) |
| Unmounting | componentWillUnmount() | return () => { ... } inside useEffect |
class UserProfile extends React.Component {
componentDidMount() {
console.log("Component mounted");
fetchUser(this.props.userId).then((user) => this.setState({ user }));
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
console.log("userId changed");
fetchUser(this.props.userId).then((user) => this.setState({ user }));
}
}
componentWillUnmount() {
console.log("Component unmounting");
}
render() {
return <div>{this.state.user ? this.state.user.name : "Loading..."}</div>;
}
}Common Mistakes
- Forgetting the dependency array entirely, which makes an effect run after every single render instead of just on mount or update.
- Forgetting to return a cleanup function for subscriptions or timers, causing memory leaks after the component unmounts.
- Assuming componentDidUpdate and useEffect with a dependency array behave identically in every edge case — useEffect runs after the mount too, unless you specifically guard against it.
- Manually comparing previous and current props inside a function component instead of simply relying on the dependency array.
Best Practices
- Use an empty dependency array for logic that should only run once, on mount.
- List every relevant value in the dependency array for logic that should respond to updates.
- Always return a cleanup function from any effect that sets up a subscription, timer, or listener.
- Think in terms of "synchronizing with an external system" rather than "mimicking a class lifecycle method" — this mental model fits useEffect better.
- When reading legacy class components, use the mounting/updating/unmounting table as a quick translation guide to the Hooks equivalent.
Frequently Asked Questions
Does every component go through all three phases?
Yes. Every component mounts once, may update any number of times (including zero), and eventually unmounts when it is removed from the screen.
Can one useEffect call handle mounting, updating, and unmounting all at once?
Yes. The function you pass runs on mount and on every update that changes a listed dependency, and the function it returns runs as cleanup before the next run and on final unmount.
Are class lifecycle methods deprecated?
They still work and are supported, but the official React documentation recommends function components with Hooks for all new code. You mainly need to recognize them when reading older code.
Does componentDidUpdate run on the very first render?
No, only useEffect does (when there is no dependency array, or the dependency array condition is met on mount). componentDidUpdate specifically skips the initial mount and only fires on subsequent updates.
What happens if I forget the cleanup function for an interval?
The interval keeps running even after the component is removed from the screen, silently wasting resources and potentially causing errors if it tries to update state on an unmounted component.
Key Takeaways
- Every component passes through three phases: mounting, updating, and unmounting.
- useEffect(fn, []) maps to mounting — the function runs once after the first render.
- useEffect(fn, [dep]) maps to updating — the function reruns whenever dep changes.
- The cleanup function returned from useEffect maps to unmounting.
- Class components express the same phases with componentDidMount, componentDidUpdate, and componentWillUnmount.
Summary
Every React component moves through the same three-phase lifecycle: mounting, updating, and unmounting. Function components express all three through useEffect and its dependency array and cleanup function, replacing what used to require three separate named methods on a class.
In this lesson, you walked through each lifecycle phase individually, saw how they combine in a single effect, and compared the Hooks approach to the older class lifecycle methods. Next, you will learn about the Context API, which solves the problem of passing data through many layers of components.
- You can name and explain the three component lifecycle phases.
- You can map mounting, updating, and unmounting to useEffect and its cleanup function.
- You can translate class lifecycle methods to their Hooks equivalents.
- You are ready to explore the Context API next.