Skip to content
GeeksSmith
advanced 9 min read

React Class Component Lifecycle vs Hooks Execution Mapping

React class components manage lifecycle through instance methods (`componentDidMount`, `componentDidUpdate`, `shouldComponentUpdate`), while functional components synchronize state with side-effects using the declarative hook pipeline (`useEffect`, `useLayoutEffect`, `useMemo`, `useRef`).

Why it matters in interviews

Tested extensively in Tekion Corp Round 1, Walmart, and Meta. Candidates must explain the exact 1-to-1 lifecycle mappings, stale closures vs mutable instance state, React 18 automatic batching, and why Error Boundaries still require class components.

Visual & Interactive Explanation

Class Component Lifecycle vs React Hooks Mapping

Interactive Comparison

Class Component Lifecycle

Imperative Lifecycle Methods

Legacy / Class
MountingcomponentDidMount()
UpdatingcomponentDidUpdate(prevProps, prevState)
UnmountingcomponentWillUnmount()
BailoutshouldComponentUpdate(nextProps)
DOM MeasuregetSnapshotBeforeUpdate()
ErrorscomponentDidCatch() / getDerivedStateFromError()

Modern React Hooks

Declarative Effect Synchronization

Modern Standard
MountinguseEffect(() => {}, [])
UpdatinguseEffect(() => {}, [deps])
Unmountingreturn () => { /* cleanup */ }
BailoutReact.memo(Component, arePropsEqual)
DOM MeasureuseLayoutEffect() / useInsertionEffect()
ErrorsErrorBoundary class component (hooks pending)

Code Examples & Implementation

Direct Lifecycle Comparison: Data Fetching & Window Listener
// --- 1. CLASS COMPONENT (Fragmented Logic) ---
class UserProfileClass extends React.Component<{ userId: string }, { user: any; width: number }> {
  state = { user: null, width: window.innerWidth };

  componentDidMount() {
    this.fetchUser(this.props.userId);
    window.addEventListener("resize", this.handleResize);
  }

  componentDidUpdate(prevProps: { userId: string }) {
    if (prevProps.userId !== this.props.userId) {
      this.fetchUser(this.props.userId);
    }
  }

  componentWillUnmount() {
    window.removeEventListener("resize", this.handleResize);
  }

  handleResize = () => this.setState({ width: window.innerWidth });
  fetchUser = (id: string) => fetchUser(id).then((u) => this.setState({ user: u }));

  render() {
    return <div>{this.state.user?.name} (Width: {this.state.width})</div>;
  }
}

// --- 2. FUNCTIONAL COMPONENT WITH HOOKS (Colocated Concerns) ---
function UserProfileHooks({ userId }: { userId: string }) {
  const [user, setUser] = useState(null);
  const [width, setWidth] = useState(() => (typeof window !== "undefined" ? window.innerWidth : 0));

  // Concern A: Data sync
  useEffect(() => {
    let active = true;
    fetchUser(userId).then((u) => {
      if (active) setUser(u);
    });
    return () => { active = false; };
  }, [userId]);

  // Concern B: Window resize
  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  return <div>{user?.name} (Width: {width})</div>;
}
Stale Closure vs Mutable Instance Trap (Classic Interview Problem)
// CLASS: Always logs latest state because 'this.state' is mutated in place
class ClassCounter extends React.Component {
  state = { count: 0 };
  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
    setTimeout(() => {
      console.log("Class count after 2s:", this.state.count); // Logs LATEST count!
    }, 2000);
  };
}

// HOOKS: Captures 'count' at the moment of click (Closure Snapshot)
function HookCounter() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
    setTimeout(() => {
      console.log("Hook count after 2s:", count); // Logs SNAPSHOT value (e.g. 0)!
    }, 2000);
  };
  // Fix for latest value: use a ref (useRef) to emulate mutable instance
}

Interview-Ready Answers

`componentDidMount` maps to `useEffect(..., [])`, `componentDidUpdate` maps to `useEffect(..., [deps])`, `componentWillUnmount` maps to effect cleanup functions, and `shouldComponentUpdate` maps to `React.memo`.

Follow-up Questions & Deep Dives

🎯 Code Output — What Does This Print?

Question 1hard
function Counter() {
  const [count, setCount] = useState(0);

  const showCount = () => {
    setTimeout(() => {
      console.log("Count:", count);
    }, 1000);
  };

  return (
    <div>
      <button onClick={() => { setCount(count + 1); showCount(); }}>
        Increment & Log
      </button>
    </div>
  );
}
// User clicks the button once. What is logged after 1 second?

Common Mistakes & Anti-Patterns

  • Thinking `useEffect` runs synchronously before paint like `componentDidMount` did
  • Missing cleanup return functions on effects that subscribe to WebSockets or event listeners
  • Attempting to write an Error Boundary using functional hooks
  • Assuming `this.state` in classes and `useState` in functional components behave identically during async callbacks

Implementation Evolution: Anti-Pattern to Production

❌ Anti-Pattern: Treating `useEffect` as a Strict Imperative Lifecycle Method

// ❌ Anti-Pattern: Omitting dependencies to mimic "componentDidMount"
function ChatRoom({ roomId }: { roomId: string }) {
  useEffect(() => {
    const connection = createConnection(roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // ❌ ESLint warning! roomId change will NOT reconnect to new room!

  return <div>Chat in room #{roomId}</div>;
}
Why this breaks: Trying to force `useEffect` to behave like `componentDidMount` by leaving `deps: []` empty results in stale closure bugs: when `roomId` changes, the component stays connected to the old room.

✅ Correct Pattern: Declarative Synchronization with Dependency Array

// ✅ Correct: Declare all used reactive values in dependencies
function ChatRoom({ roomId }: { roomId: string }) {
  useEffect(() => {
    const connection = createConnection(roomId);
    connection.connect();

    // Clean up old connection before connecting to next room
    return () => connection.disconnect();
  }, [roomId]);

  return <div>Chat in room #{roomId}</div>;
}
🚀

🚀 Production-Grade (Hardened): Production ErrorBoundary Component (Why Classes are Still Required)

// 🚀 Production: React ErrorBoundary (No hook equivalent exists in React 18/19)
import React, { Component, ErrorInfo, ReactNode } from "react";

interface Props {
  fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
  children: ReactNode;
}

interface State {
  hasError: boolean;
  error: Error | null;
}

export class ErrorBoundary extends Component<Props, State> {
  public state: State = { hasError: false, error: null };

  public static getDerivedStateFromError(error: Error): State {
    // Update state so the next render will show the fallback UI
    return { hasError: true, error };
  }

  public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    // Log error to telemetry (e.g. Sentry / Datadog)
    console.error("ErrorBoundary caught:", error, errorInfo);
  }

  public reset = () => {
    this.setState({ hasError: false, error: null });
  };

  public render() {
    if (this.state.hasError && this.state.error) {
      if (typeof this.props.fallback === "function") {
        return this.props.fallback(this.state.error, this.reset);
      }
      return this.props.fallback || (
        <div className="p-4 border border-signal-red bg-signal-red/10 rounded-lg text-signal-red">
          <h3>Something went wrong.</h3>
          <button onClick={this.reset} className="mt-2 px-3 py-1 bg-signal-red text-white rounded">
            Try Again
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

⚠️ Trick Questions & Interviewer Traps

Q1:Why does React not provide a `useErrorBoundary` or `useCatch` hook?

Q2:How does `useLayoutEffect` differ from `componentDidMount`?

📋 Rapid Revision Cheat Sheet

  • Mount: `componentDidMount` -> `useEffect(() => {}, [])`.
  • Update: `componentDidUpdate` -> `useEffect(() => {}, [deps])`.
  • Unmount: `componentWillUnmount` -> `return () => { /* cleanup */ }`.
  • Bailout: `shouldComponentUpdate` -> `React.memo(Component, arePropsEqual)`.
  • DOM Measurement: `getSnapshotBeforeUpdate` -> `useLayoutEffect`.
  • Error Handling: `componentDidCatch` (Requires Class Component ErrorBoundary).
  • Closure Trap: Hooks capture state snapshots; Classes read mutable `this.state`.

Real-World Architectural Scenario

A candidate refactors an old class component with a 3-second setTimeout alert to a functional component with hooks. When the user clicks the button and changes input text within 3 seconds, the alert shows the OLD text instead of the new text. Why?

Rate Your Readiness

Rate how comfortably you can explain this in an interview to update your global readiness gauge:

Rate your confidence: