Skip to content

React Lifecycle Explained — Component Mount, Update, and Unmount

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about React Lifecycle Explained. We cover key concepts, practical examples, and best practices to help you master this topic.

React component lifecycle describes the stages a component goes through from creation to destruction: mounting (birth), updating (growth), and unmounting (death).

What You'll Learn

  • The three lifecycle phases: mount, update, unmount
  • How useEffect maps to lifecycle events
  • How to run code on mount (initialization)
  • How to run code on update (reacting to changes)
  • How to clean up on unmount

Why It Matters

Understanding the lifecycle helps you place code in the right phase. Fetching data on mount, validating on update, and cleaning up on unmount prevents bugs and memory leaks.

Real-World Use

Durga Antivirus Pro's real-time scanner starts a Websocket connection on mount, reconnects on network changes (update), and closes the connection on unmount to prevent resource leaks.

flowchart LR
    A[Mount] --> B[Render]
    B --> C[useEffect Mount]
    C --> D[Update]
    D --> E[Re-render]
    E --> F[useEffect Cleanup]
    F --> G[useEffect Update]
    G --> H[Unmount]
    H --> I[useEffect Cleanup]
    style A fill:#3b82f6,color:#fff

Mounting Phase

Mounting is when the component first appears in the DOM:

import { useState, useEffect } from "react";

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  // Runs once on mount (when userId changes)
  useEffect(() => {
    console.log("Component mounted. Fetching user:", userId);

    const controller = new AbortController();

    async function fetchUser() {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`, {
          signal: controller.signal
        });
        const data = await response.json();
        setUser(data);
        setLoading(false);
      } catch (err) {
        if (err.name !== "AbortError") {
          console.error("Failed to fetch user:", err);
          setLoading(false);
        }
      }
    }

    fetchUser();

    // Cleanup function runs on unmount or before next effect
    return () => {
      console.log("Cleanup: aborting fetch");
      controller.abort();
    };
  }, [userId]); // Re-runs when userId changes

  if (loading) return <div>Loading user...</div>;
  if (!user) return <div>User not found</div>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

Expected output: Fetching user data on mount, canceling the request on unmount, and re-fetching when userId changes.

The useEffect with [userId] dependency runs on mount and whenever userId changes. The cleanup function runs before the next effect and on unmount. This replaces componentDidMount, componentDidUpdate, and componentWillUnmount from class components.

Updating Phase

The updating phase happens when props or state change:

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [searchHistory, setSearchHistory] = useState([]);

  // Effect for search - runs when query changes
  useEffect(() => {
    if (!query) return;

    const fetchResults = async () => {
      const response = await fetch(`/api/search?q=${query}`);
      const data = await response.json();
      setResults(data);
    };

    fetchResults();
  }, [query]);

  // Effect for logging - runs when results change
  useEffect(() => {
    if (results.length > 0) {
      console.log(`Search returned ${results.length} results`);
    }
  }, [results]);

  // Effect without dependencies - runs on every render
  useEffect(() => {
    console.log("Component rendered");
    // Avoid: this runs on every render, including state changes
  }); // No dependency array

  return (
    <div>
      {results.map(item => (
        <div key={item.id}>{item.title}</div>
      ))}
    </div>
  );
}

Expected output: Search results update when query changes. The log shows result count when results update. The render log fires on every render.

Understanding dependencies is critical. An empty array [] runs once. No array runs on every render. The array tells React when to re-run the effect.

Unmounting Phase

Clean up resources when the component leaves the DOM:

import { useState, useEffect } from "react";

function LiveClock() {
  const [time, setTime] = useState(new Date());

  useEffect(() => {
    console.log("Clock started");

    const interval = setInterval(() => {
      setTime(new Date());
    }, 1000);

    // Cleanup function
    return () => {
      console.log("Clock stopped");
      clearInterval(interval);
    };
  }, []); // Empty array = run once on mount

  return <p>Current time: {time.toLocaleTimeString()}</p>;
}

function ParentComponent() {
  const [showClock, setShowClock] = useState(true);

  return (
    <div>
      <button onClick={() => setShowClock(!showClock)}>
        {showClock ? "Hide" : "Show"} Clock
      </button>
      {showClock && <LiveClock />}
    </div>
  );
}

Expected output: Clicking "Hide Clock" destroys the LiveClock component. The cleanup function clears the interval, preventing memory leaks.

Every useEffect that creates a subscription, timer, or event listener must return a cleanup function. Without it, the resource lives on after the component is gone.

Strict Mode Double-Mounting

In development with StrictMode, effects run twice:

import { StrictMode } from "react";

function DevEffect() {
  useEffect(() => {
    console.log("Effect runs");
    return () => console.log("Cleanup runs");
  }, []);

  // In StrictMode dev: Cleanup -> Effect -> Cleanup -> Effect
  // This helps find missing cleanup functions
  return <p>Check console</p>;
}

// In index.js
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

Expected output: In development, you see "Effect runs", "Cleanup runs", "Effect runs", "Cleanup runs" in sequence.

StrictMode intentionally double-invokes effects in development to detect bugs: missing cleanup functions, incorrect dependency arrays, and impure effects. This does not happen in production.

Class Component Lifecycle

Legacy class lifecycle methods and their useEffect equivalents:

// Class component
class UserPage extends React.Component {
  componentDidMount() {
    // On mount
    fetchUser(this.props.userId);
  }

  componentDidUpdate(prevProps) {
    // On update, when userId changed
    if (prevProps.userId !== this.props.userId) {
      fetchUser(this.props.userId);
    }
  }

  componentWillUnmount() {
    // On unmount
    cleanup();
  }

  render() { return <div>...</div>; }
}

// Equivalent with hooks
function UserPage({ userId }) {
  useEffect(() => {
    fetchUser(userId);
    return () => cleanup();
  }, [userId]);
}

Expected output: The hook version is more concise. A single useEffect with [userId] replaces componentDidMount, componentDidUpdate, and componentWillUnmount.

Hooks simplify lifecycle management. Instead of splitting logic across three methods, you co-locate related code in one effect. The dependency array makes the intent explicit.

Common Mistakes

  1. Missing dependency arrays — Forgetting the array causes infinite re-renders. Always specify dependencies.

  2. Incorrect dependencies — Using stale variables in effects without listing them causes bugs. The linter (react-hooks/exhaustive-deps) catches this.

  3. Not cleaning up subscriptions — Intervals, WebSockets, and event listeners leak memory if not cleaned up in the effect's return function.

  4. Fetching data without abort — If the component unmounts before fetch completes, setState on unmounted component causes warnings. Use AbortController.

  5. Putting too much in one effect — Split unrelated logic into separate useEffect calls. Data fetching and logging should not share an effect.

Practice Questions

  1. What are the three lifecycle phases? Mounting (component appears), updating (props/state change), unmounting (component is removed).

  2. How do you run code only on mount? useEffect(() => { ... }, []) with an empty dependency array.

  3. How do you run code when a specific prop changes? Include the prop in the dependency array: useEffect(() => { ... }, [propName]).

  4. What does the cleanup function in useEffect do? It runs before the component unmounts and before the effect re-runs. Use it to cancel subscriptions, timers, or fetch requests.

  5. Why does StrictMode double-fire effects? To help detect missing cleanup functions and impure effects during development.

Challenge

Build a ConnectionStatus component that monitors online/offline status. Use useEffect to add online/offline event listeners on mount and clean them up on unmount. Show a banner when offline. Simulate the effect by temporarily going offline in DevTools.

FAQ

Can I have multiple useEffect in one component?

Yes. Separate concerns into different effects for clarity and correct dependencies.

What happens if I omit the dependency array?

The effect runs after every render. Use this only for non-side-effect logging or debugging.

Does useEffect run synchronously?

No, effects run after the browser paints, making them non-blocking for the initial render.

What is the cleanup function pattern?

Return a function from useEffect. React calls it before the effect re-runs and on unmount.

How do I run an effect only on update, not mount?

Use a ref to track whether the component mounted: if (didMount.current) { ... }.

Mini Project

Build a WebSocketChat component that connects to a WebSocket on mount, reconnects with exponential backoff on connection loss (update phase), and disconnects on unmount. Display messages as they arrive. Show connection status indicator. Use useEffect with cleanup for all lifecycle phases. Test by toggling the component visibility.

What's Next

Continue with effects and refs:

React Effects, React Refs, React State

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro