React Interview Prep
Lifecycle and Rendering

Component Lifecycle with Hooks

Component Lifecycle with Hooks

LinkedIn Hook

Class components had componentDidMount, componentDidUpdate, and componentWillUnmount. Three separate methods. Three separate mental models.

Hooks replaced all of them with a single function: useEffect.

But here is the problem most developers run into — they try to map useEffect 1:1 to class lifecycle methods, and it breaks their mental model. useEffect does not think in terms of "mount" and "update." It thinks in terms of synchronization.

In interviews, lifecycle questions are everywhere. "What runs on mount?" "How do you clean up a subscription?" "What happens when a dependency changes?" The candidates who stumble are the ones who memorized the class lifecycle diagram but never understood how hooks actually schedule and clean up effects.

In this lesson, I break down mounting, updating, and unmounting with hooks — the exact mental model, real code with outputs, a class-to-hooks comparison table, and the interview questions you will actually face.

If you have ever been confused about when useEffect runs, when the cleanup fires, or how to explain the lifecycle in a hooks world — this one is for you.

Read the full lesson -> [link]

#React #JavaScript #InterviewPrep #Frontend #CodingInterview #ReactHooks #useEffect #ComponentLifecycle #100DaysOfCode


Component Lifecycle with Hooks thumbnail


What You'll Learn

  • How React components go through mounting, updating, and unmounting phases
  • How useEffect with an empty dependency array simulates componentDidMount
  • How useEffect with dependencies handles the update phase
  • How the cleanup return function replaces componentWillUnmount
  • The exact execution order of effects and cleanups
  • A direct comparison table mapping class lifecycle methods to hooks

The Concept — Component Lifecycle in a Hooks World

Analogy: The Apartment Tenant

Think of a React component as a tenant moving into an apartment.

Mounting is move-in day. You sign the lease, unpack your furniture, and set up your internet connection. In React, this is the first render — the component appears in the DOM for the first time, and you set up subscriptions, fetch data, or start timers.

Updating is living there. Every time your situation changes — new furniture, a roommate moves in, you switch internet providers — you adjust. You tear down the old internet plan before setting up the new one. In React, when props or state change, the component re-renders, the previous effect's cleanup runs, and the new effect runs.

Unmounting is moving out. You cancel the internet, return the keys, clean the apartment. In React, the component is removed from the DOM, and the cleanup function runs one final time to tear down everything.

useEffect is the lease agreement. It says: "When you move in (mount), do this setup. When things change (update), redo the setup. When you move out (unmount), run the cleanup." One function handles all three phases.


Phase 1: Mounting — useEffect with []

When you pass an empty dependency array, the effect runs once after the first render. This is the hooks equivalent of componentDidMount.

Code Example 1: Setup on Mount

import { useState, useEffect } from "react";

function UserDashboard({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // This runs ONCE after the component first renders (mount)
    console.log("Effect: Component mounted, fetching user...");

    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((data) => setUser(data));

    // No cleanup needed for a one-time fetch
  }, []); // Empty array = run only on mount

  return (
    <div>
      {user ? <h1>Welcome, {user.name}</h1> : <p>Loading...</p>}
    </div>
  );
}

// Timeline:
// 1. React renders the component -> DOM shows "Loading..."
// 2. useEffect fires AFTER the paint -> Console: "Effect: Component mounted, fetching user..."
// 3. Fetch completes -> setUser triggers re-render -> DOM shows "Welcome, Alice"
// 4. useEffect does NOT run again (empty deps = mount only)

Key point: useEffect runs after the browser paints. The user sees the initial render first, then the effect fires. This is different from componentDidMount in class components, which fires before the browser paints (synchronously after render in the commit phase). For most cases this distinction does not matter, but it is a sharp interview detail.


Phase 2: Updating — useEffect with Dependencies

When you provide dependencies, the effect re-runs whenever any dependency value changes. Before running the new effect, React runs the previous effect's cleanup first.

Code Example 2: Effect That Responds to Changes

import { useState, useEffect } from "react";

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    // This runs on mount AND every time roomId changes
    console.log(`Effect: Connecting to room ${roomId}`);

    const connection = createWebSocket(roomId);

    connection.onMessage((msg) => {
      setMessages((prev) => [...prev, msg]);
    });

    // Cleanup: runs BEFORE the next effect and on unmount
    return () => {
      console.log(`Cleanup: Disconnecting from room ${roomId}`);
      connection.close();
    };
  }, [roomId]); // Re-run when roomId changes

  return (
    <ul>
      {messages.map((msg, i) => (
        <li key={i}>{msg}</li>
      ))}
    </ul>
  );
}

// Timeline when roomId changes from "general" to "random":
// 1. React re-renders with roomId = "random"
// 2. Console: "Cleanup: Disconnecting from room general"  (old effect's cleanup)
// 3. Console: "Effect: Connecting to room random"          (new effect runs)
//
// On unmount (component removed from DOM):
// 4. Console: "Cleanup: Disconnecting from room random"   (final cleanup)

The mental model: React does not think "mount" vs "update." It thinks: "The dependencies changed, so I need to re-synchronize. Tear down the old effect, set up the new one." This is why the React docs say useEffect is about synchronization, not lifecycle.

Component Lifecycle with Hooks visual 1


Phase 3: Unmounting — The Cleanup Return

The function you return from useEffect is the cleanup. It runs in two situations: before the effect re-runs (on update) and when the component is removed from the DOM (unmount). This replaces componentWillUnmount.

Code Example 3: Timer Cleanup on Unmount

import { useState, useEffect } from "react";

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

  useEffect(() => {
    // Setup: start a timer on mount
    console.log("Effect: Starting timer");
    const intervalId = setInterval(() => {
      setSeconds((prev) => prev + 1);
    }, 1000);

    // Cleanup: clear the timer on unmount
    return () => {
      console.log("Cleanup: Clearing timer");
      clearInterval(intervalId);
    };
  }, []); // Empty deps = setup once, cleanup on unmount

  return <p>Time: {seconds}s</p>;
}

function App() {
  const [showTimer, setShowTimer] = useState(true);

  return (
    <div>
      <button onClick={() => setShowTimer((prev) => !prev)}>
        {showTimer ? "Hide" : "Show"} Timer
      </button>
      {showTimer && <Stopwatch />}
    </div>
  );
}

// User clicks "Hide Timer":
// 1. React removes <Stopwatch> from the DOM (unmount)
// 2. Console: "Cleanup: Clearing timer"
// 3. The interval is properly cleared — no memory leak
//
// Without the cleanup:
// The interval would keep running in the background even after
// the component is gone, calling setSeconds on an unmounted component.

Class Lifecycle to Hooks — Comparison Table

Code Example 4: The Same Component in Both Styles

// === CLASS COMPONENT ===
class UserProfile extends React.Component {
  state = { user: null };

  componentDidMount() {
    // Runs once after first render
    this.fetchUser(this.props.userId);
    document.title = `Profile: ${this.props.userId}`;
  }

  componentDidUpdate(prevProps) {
    // Runs after every re-render (must compare manually)
    if (prevProps.userId !== this.props.userId) {
      this.fetchUser(this.props.userId);
      document.title = `Profile: ${this.props.userId}`;
    }
  }

  componentWillUnmount() {
    // Runs when component is removed
    document.title = "App";
  }

  fetchUser(id) {
    fetch(`/api/users/${id}`)
      .then((res) => res.json())
      .then((user) => this.setState({ user }));
  }

  render() {
    return this.state.user
      ? <h1>{this.state.user.name}</h1>
      : <p>Loading...</p>;
  }
}

// === HOOKS EQUIVALENT ===
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // Covers both mount AND update (when userId changes)
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((data) => setUser(data));

    document.title = `Profile: ${userId}`;

    // Covers unmount AND cleanup before re-run
    return () => {
      document.title = "App";
    };
  }, [userId]); // React handles the "did this change?" check for you

  return user ? <h1>{user.name}</h1> : <p>Loading...</p>;
}

// The class version: 3 lifecycle methods, manual prop comparison in componentDidUpdate
// The hooks version: 1 useEffect, dependency array handles comparison automatically

Component Lifecycle with Hooks visual 2


The Interview Comparison Table

+-----------------------------+-------------------------------+------------------------------+
| Phase                       | Class Method                  | Hooks Equivalent             |
+-----------------------------+-------------------------------+------------------------------+
| First render complete       | componentDidMount()           | useEffect(() => {}, [])      |
+-----------------------------+-------------------------------+------------------------------+
| After every re-render       | componentDidUpdate()          | useEffect(() => {})          |
|                             |                               | (no dependency array)        |
+-----------------------------+-------------------------------+------------------------------+
| When specific values change | componentDidUpdate()          | useEffect(() => {}, [a, b]) |
|                             | + manual prevProps comparison |                              |
+-----------------------------+-------------------------------+------------------------------+
| Before removal from DOM     | componentWillUnmount()        | useEffect cleanup return     |
+-----------------------------+-------------------------------+------------------------------+
| Before re-running effect    | (no direct equivalent)        | useEffect cleanup return     |
+-----------------------------+-------------------------------+------------------------------+
| Error boundary              | componentDidCatch()           | No hook equivalent           |
|                             | getDerivedStateFromError()    | (still needs a class or      |
|                             |                               |  a library like react-       |
|                             |                               |  error-boundary)             |
+-----------------------------+-------------------------------+------------------------------+
| Conditional side effects    | if/else inside                | Multiple useEffect calls     |
|                             | componentDidUpdate            | with different deps          |
+-----------------------------+-------------------------------+------------------------------+

Key interview takeaway: Hooks do not have a 1:1 mapping to class lifecycle. The mental model shifted from "respond to lifecycle events" to "synchronize with external systems when dependencies change." This is the answer interviewers want to hear.


Common Mistakes

Mistake 1: Forgetting the Dependency Array (Effect Runs Every Render)

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

  useEffect(() => {
    // BUG: No dependency array means this runs after EVERY render
    // Typing one character -> fetch -> setState -> re-render -> fetch -> ...
    fetch(`/api/search?q=${query}`)
      .then((res) => res.json())
      .then((data) => setResults(data));
  }); // Missing dependency array!

  return <ul>{results.map((r) => <li key={r.id}>{r.title}</li>)}</ul>;
}

// FIX: Add [query] as the dependency array.
// useEffect(() => { ... }, [query]);
// Now it only re-runs when query actually changes.

Mistake 2: Using an Object or Array as a Dependency Without Memoizing

function Dashboard({ filters }) {
  useEffect(() => {
    // BUG: If the parent creates a new filters object on every render,
    // this effect re-runs every time — even if the filter values are identical.
    // Objects are compared by reference, not by value.
    fetchData(filters);
  }, [filters]); // Reference changes every render = infinite re-fetching

  return <div>...</div>;
}

// FIX: Either destructure primitives into the dependency array:
// const { category, sort } = filters;
// useEffect(() => { fetchData({ category, sort }); }, [category, sort]);
//
// Or memoize the filters object in the parent with useMemo.

Mistake 3: Missing Cleanup for Subscriptions and Timers

function Notifications() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // BUG: No cleanup — if this component mounts/unmounts multiple times,
    // you accumulate multiple event listeners (memory leak)
    window.addEventListener("push-notification", () => {
      setCount((prev) => prev + 1);
    });
  }, []);

  return <p>Notifications: {count}</p>;
}

// FIX: Always return a cleanup function for subscriptions.
// useEffect(() => {
//   const handler = () => setCount((prev) => prev + 1);
//   window.addEventListener("push-notification", handler);
//   return () => window.removeEventListener("push-notification", handler);
// }, []);

Interview Questions

Q: What is the equivalent of componentDidMount in functional components?

useEffect with an empty dependency array []. The effect runs once after the first render. However, the mental model is different — in hooks, you are not thinking "run this on mount." You are thinking "synchronize with this external system, and it has no dependencies, so it only needs to synchronize once."

Q: How does the cleanup function in useEffect work? When does it run?

The cleanup function (the function returned from useEffect) runs in two scenarios: (1) before the effect re-runs due to a dependency change, and (2) when the component unmounts. React always runs the previous cleanup before executing the next effect. This ensures you never have stale subscriptions or dangling timers. It replaces componentWillUnmount but is more powerful because it also cleans up between updates.

Q: What happens if you omit the dependency array entirely from useEffect?

The effect runs after every single render — both the initial render and every subsequent re-render. This is rarely what you want. It behaves like componentDidMount + componentDidUpdate combined with no conditions. For most effects, you should provide a dependency array to control when the effect re-runs.

Q: Can you have multiple useEffect calls in one component? Why would you?

Yes, and it is encouraged. Each useEffect should handle one concern. In class components, you often mixed unrelated logic in componentDidMount (start a timer AND fetch data AND subscribe to events). With hooks, you split these into separate useEffect calls, each with its own dependencies and cleanup. This makes the code easier to read, test, and maintain.

Q: How does useEffect differ from useLayoutEffect?

useEffect runs asynchronously after the browser has painted the screen. useLayoutEffect runs synchronously after the DOM mutation but before the browser paints. Use useLayoutEffect when you need to read layout (like measuring an element's dimensions) and make a visual change before the user sees the initial render. In practice, useEffect is correct for 95% of cases. Interviewers ask this to check whether you understand the rendering pipeline.


Quick Reference — Cheat Sheet

+-----------------------------------+-------------------------------------------+
| Concept                           | Key Point                                 |
+-----------------------------------+-------------------------------------------+
| useEffect(() => {}, [])           | Runs once after first render (mount).     |
|                                   | Equivalent to componentDidMount.          |
+-----------------------------------+-------------------------------------------+
| useEffect(() => {}, [a, b])       | Runs after render when a or b changes.    |
|                                   | Replaces componentDidUpdate with          |
|                                   | automatic comparison.                     |
+-----------------------------------+-------------------------------------------+
| useEffect(() => {})               | Runs after every render. Rarely desired.  |
|                                   | Missing deps array is usually a bug.      |
+-----------------------------------+-------------------------------------------+
| return () => cleanup              | Runs before next effect and on unmount.   |
|                                   | Replaces componentWillUnmount + more.     |
+-----------------------------------+-------------------------------------------+
| Multiple useEffects               | Split unrelated logic into separate       |
|                                   | effects. One concern per effect.          |
+-----------------------------------+-------------------------------------------+
| useEffect vs useLayoutEffect      | useEffect: after paint (async).           |
|                                   | useLayoutEffect: before paint (sync).     |
+-----------------------------------+-------------------------------------------+
| Class lifecycle mapping           | Mount = [], Update = [deps],              |
|                                   | Unmount = cleanup return.                 |
+-----------------------------------+-------------------------------------------+
| Error boundaries                  | No hook equivalent. Use class components  |
|                                   | or react-error-boundary library.          |
+-----------------------------------+-------------------------------------------+

RULE: Think synchronization, not lifecycle.
RULE: Always provide a dependency array unless you intentionally want every-render execution.
RULE: Always return cleanup for subscriptions, timers, and event listeners.

Previous: Lesson 3.7 — Custom Hooks — Reusable Logic -> Next: Lesson 4.2 — Re-rendering — When & Why ->


This is Lesson 4.1 of the React Interview Prep Course — 10 chapters, 42 lessons.

On this page