LearnContact
Lesson 3423 min read

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.

What You Will Learn
  • 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 — component is created and rendered for the first time
Updating — component re-renders in response to new props or state
Unmounting — component is removed from the screen 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.

Mounting with useEffect
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>;
}
Console Output
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.

Updating with useEffect
useEffect(() => {
  console.log("userId changed — re-fetching user data");
  fetchUser(userId).then(setUser);
}, [userId]); // reruns whenever userId changes
Console Output
userId changed — re-fetching user data
(logged again each time the userId prop changes to a new value)
One Hook, Two Phases

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.

Unmounting via the cleanup function
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);
  };
}, []);
Console Output
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.

All three phases in one effect
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]);
Mount: effect runs, subscribes to userId
Update: cleanup runs, then effect runs again for new userId
Unmount: cleanup runs one final time

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.

PhaseClass Component MethodFunction Component Equivalent
MountingcomponentDidMount()useEffect(fn, [])
UpdatingcomponentDidUpdate(prevProps, prevState)useEffect(fn, [dep])
UnmountingcomponentWillUnmount()return () => { ... } inside useEffect
The same logic as a class component (for reference)
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

Avoid These 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.

Lesson 34 Completed
  • 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.
Next Lesson →

Context API