Senior 17 min · March 05, 2026

React Lifecycle Methods — Why Your setInterval Leaks Memory

Dashboard tab consumes 200MB immediately, climbs to 1.5GB+ in 30 minutes from missing clearInterval in componentWillUnmount.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • React components have three predictable phases: mounting, updating, and unmounting
  • componentDidMount runs once after DOM insertion — your safe zone for data fetching
  • componentDidUpdate fires after every re-render — always add a comparison guard
  • componentWillUnmount cleans up timers, subscriptions, and listeners — skip it and you leak memory
  • useEffect collapses all three phases into one API with dependency array control
  • Missing cleanup is the #1 cause of "Can't perform a React state update on an unmounted component" warnings
✦ Definition~90s read
What is React Lifecycle Methods?

React lifecycle methods are the hooks React gives you to run code at specific points during a component's existence — mounting, updating, and unmounting. They exist because React components don't just render once; they live, change, and die. Without them, you'd have no reliable way to fetch data when a component appears, react to prop changes, or clean up subscriptions like setInterval before the component disappears.

Think of a React component like a houseplant.

That last one is why your intervals leak memory: if you start an interval in componentDidMount but never clear it in componentWillUnmount, the callback keeps running against a dead DOM node, holding references that the garbage collector can't touch.

These methods form a contract: React guarantees the order and timing of calls, but you're responsible for honoring the cleanup side. The three phases are mount (constructor -> render -> componentDidMount), update (new props/state -> render -> componentDidUpdate), and unmount (componentWillUnmount).

In class components, componentDidMount is where you kick off AJAX calls, WebSocket connections, or timers. componentDidUpdate lets you compare previous and current props to avoid infinite loops. componentWillUnmount is your only chance to tear down what you started — clearInterval, removeEventListener, abort fetch requests.

In modern React with hooks, useEffect replaces all three: a single effect can run on mount, on updates (via dependency array), and on unmount (via the cleanup function). But the underlying lifecycle contract hasn't changed — it's just expressed differently.

If you're still using class components or migrating to hooks, understanding the original lifecycle is essential: it's the mental model that explains why useEffect's dependency array exists and why forgetting the cleanup function causes the same memory leaks as skipping componentWillUnmount. Tools like React DevTools profiler and Chrome's Performance tab can confirm these leaks by showing detached DOM nodes and growing heap snapshots.

Plain-English First

Think of a React component like a houseplant. When you first bring it home and pot it, that's mounting — it's coming to life. Every time you water it or it grows a new leaf, that's updating — it's reacting to change. When it finally dies and you throw it out, that's unmounting — cleanup time. Lifecycle methods are just the specific moments React taps you on the shoulder and says 'hey, something just happened to your component — do you want to do anything about it?'

Every React app you've ever used — a Twitter feed refreshing in real time, a form that validates as you type, a dashboard that polls an API every 30 seconds — depends on lifecycle methods working correctly behind the scenes. They're not a niche feature; they're the engine that drives dynamic behavior. If you've ever seen a component that shows stale data, triggers a memory leak, or fires an API call 400 times, a misunderstood lifecycle method was almost certainly the culprit.

The problem lifecycle methods solve is timing. JavaScript is asynchronous and the DOM is constantly changing, so you need a reliable way to say 'run this code ONLY after the component appears on screen' or 'clean this up BEFORE the component disappears.' Without lifecycle hooks, you'd be firing code at the wrong moment — fetching data before the component exists, or leaving open WebSocket connections after the user has navigated away.

After reading this article you'll know exactly which lifecycle method to reach for in three critical scenarios: fetching data on load, responding to prop or state changes, and cleaning up subscriptions or timers. You'll also understand the modern hooks equivalents so you can work confidently in both class-based and functional codebases.

Why React Component Lifecycle Is a Contract, Not a Timeline

React lifecycle methods are hooks into the component's existence — mount, update, unmount — that let you synchronize side effects with the render cycle. The core mechanic: React calls these methods in a deterministic order, and you use them to allocate or release resources (timers, subscriptions, network requests) that must align with the component's presence in the DOM.

In practice, the critical sequence is componentDidMount → componentDidUpdate → componentWillUnmount. componentDidMount runs once after the first render; componentWillUnmount runs once before removal. Between updates, componentDidUpdate fires after every re-render, giving you access to previous props/state for diffing. The key property: React guarantees unmount will always fire if mount fired — but only if you don't throw during render.

Use lifecycle methods when you need imperative control over resources that React's declarative model can't manage: timers, WebSocket connections, third-party DOM libraries, or analytics pings. The real-world impact: forgetting to clear a setInterval in componentWillUnmount means the callback continues executing on a dead component, leaking memory and potentially mutating unmounted state — a classic source of 'setState on unmounted component' warnings and silent data corruption.

The Unmount Guarantee Is Not Automatic
React only calls componentWillUnmount if the component actually unmounts — errors in render or async gaps can skip it, so always pair resource allocation with a cleanup that handles partial execution.
Production Insight
A real production scenario: a dashboard component fetches live stock data via setInterval every 5 seconds. The component unmounts when the user navigates away, but the interval is never cleared. The callback continues firing, calling setState on an unmounted component, causing React warnings and a memory leak that grows linearly with navigation frequency.
The exact symptom: after 10 navigations, the browser tab consumes 200+ MB of heap, and the console floods with 'Warning: Can't perform a React state update on an unmounted component'.
The rule of thumb: every setInterval in componentDidMount must have a matching clearInterval in componentWillUnmount — no exceptions. If you use hooks, the useEffect cleanup function enforces this pairing.
Key Takeaway
Lifecycle methods are a contract: mount allocates, unmount frees — break the pairing and you leak.
componentDidUpdate is for diffing, not for unconditional side effects — always compare previous and current props/state.
The unmount method is your last chance to clean up; if you miss it, the resource lives until the page reloads.
React Lifecycle Methods: Memory Leak Prevention THECODEFORGE.IO React Lifecycle Methods: Memory Leak Prevention Class lifecycle phases and their Hook equivalents for safe cleanup Mounting Phase componentDidMount: fetch data, start timers Updating Phase componentDidUpdate: respond to prop/state changes Unmounting Phase componentWillUnmount: clear timers, cancel requests useEffect Hook Combines mount, update, unmount in one effect Cleanup Function Return function from useEffect to clear resources ⚠ Missing cleanup in componentWillUnmount or useEffect return Always clear setInterval and cancel subscriptions to avoid leaks THECODEFORGE.IO
thecodeforge.io
React Lifecycle Methods: Memory Leak Prevention
React Lifecycle Methods

The Three Phases Every React Component Goes Through

React components have a predictable life. They're born (mounting), they change (updating), and they die (unmounting). Each phase gives you specific hooks to run your own code at exactly the right moment.

Mounting happens once — when the component first appears in the DOM. This is where you kick off initial data fetching, set up subscriptions, or read from localStorage. It only runs once per component instance, which is why so many developers reach for it first.

Updating happens every time props or state change. React re-renders the component and gives you lifecycle hooks both before and after that re-render. This is where you respond to changes — maybe a user selected a different user ID from a dropdown and you need to fetch that user's data.

Unmounting happens when the component is removed from the DOM — the user navigates away, a conditional renders the component out, or the parent unmounts. This is your cleanup phase. Anything you set up in mounting that isn't React-managed (timers, event listeners, WebSocket connections) must be torn down here, or you'll leak memory.

Understanding that these three phases exist — and are sequential — is the foundation everything else builds on.

LifecycleOverview.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import React, { Component } from 'react';

// A simple component that logs each lifecycle phase clearly
// so you can see the ORDER in which they fire in the console
class UserProfileCard extends Component {
  constructor(props) {
    super(props);
    // constructor runs first — before anything is painted to the DOM
    this.state = {
      profileData: null,
      isLoading: true,
    };
    console.log('[1] constructor — component instance created');
  }

  // Fires ONCE after the component appears in the DOM
  componentDidMount() {
    console.log('[3] componentDidMount — component is now visible in the DOM');

    // Safe to fetch data here because the DOM node exists
    this.fetchUserProfile(this.props.userId);
  }

  // Fires after EVERY re-render caused by props or state change
  componentDidUpdate(previousProps, previousState) {
    console.log('[5] componentDidUpdate — something changed and React re-rendered');

    // IMPORTANT: always compare previous vs current before acting
    // Without this check you get an infinite loop (fetch → setState → re-render → fetch...)
    if (previousProps.userId !== this.props.userId) {
      console.log('userId prop changed — fetching new profile');
      this.fetchUserProfile(this.props.userId);
    }
  }

  // Fires just before the component is removed from the DOM
  componentWillUnmount() {
    console.log('[6] componentWillUnmount — time to clean up');
    // If we had a timer or subscription set up in componentDidMount,
    // we'd cancel it here to avoid memory leaks
    clearTimeout(this.refreshTimer);
  }

  fetchUserProfile(userId) {
    this.setState({ isLoading: true });

    // Simulating an API call with setTimeout
    this.refreshTimer = setTimeout(() => {
      this.setState({
        profileData: { id: userId, name: 'Ada Lovelace', role: 'Engineer' },
        isLoading: false,
      });
      console.log('[4] setState called — triggers re-render and componentDidUpdate');
    }, 500);
  }

  render() {
    // render() fires BEFORE componentDidMount on first load
    // and BEFORE componentDidUpdate on subsequent renders
    console.log('[2/re-render] render — React is building the virtual DOM');

    const { isLoading, profileData } = this.state;

    if (isLoading) return <p>Loading profile...</p>;

    return (
      <div className="profile-card">
        <h2>{profileData.name}</h2>
        <p>Role: {profileData.role}</p>
      </div>
    );
  }
}

export default UserProfileCard;
Output
[1] constructor — component instance created
[2/re-render] render — React is building the virtual DOM
[3] componentDidMount — component is now visible in the DOM
// ...500ms later after setTimeout resolves...
[4] setState called — triggers re-render and componentDidUpdate
[2/re-render] render — React is building the virtual DOM
[5] componentDidUpdate — something changed and React re-rendered
// ...if userId prop changes...
[5] componentDidUpdate — something changed and React re-rendered
userId prop changed — fetching new profile
// ...on unmount...
[6] componentWillUnmount — time to clean up
The Order Matters:
constructor → render → componentDidMount → (state/props change) → render → componentDidUpdate → componentWillUnmount. Memorise this sequence and you'll instantly know WHY your code is or isn't running when you expect it to.
Production Insight
Most devs assume constructor is the right place for data fetching because it runs first.
It runs before the DOM exists — any setState here is ignored and any DOM query fails silently.
Rule: constructor is for state initialisation only. componentDidMount is for side effects.
Key Takeaway
Lifecycle methods fire in a fixed, predictable order.
Learn the sequence once and you eliminate 80% of timing bugs.
The sequence is: constructor → render → didMount → render → didUpdate → willUnmount.

componentDidMount — Your Go-To for Data Fetching and Subscriptions

componentDidMount is the most commonly used lifecycle method, and for good reason — it fires exactly once, right after the component is painted to the screen. The DOM node exists, refs are populated, and you're guaranteed a real browser environment. That makes it the perfect place to kick off async operations.

The key insight most tutorials miss: you're not just 'fetching data here because that's the pattern.' You're fetching here because React guarantees the component is mounted before this runs, so any setState calls you make in response to the fetch will trigger a re-render correctly. If you tried to call setState before mounting, React would either throw or silently discard your update.

Beyond data fetching, componentDidMount is where you set up anything that needs a real DOM node — third-party charting libraries, manual event listeners on window or document, or WebSocket connections. The golden rule: if you set it up here, you must tear it down in componentWillUnmount.

With hooks, componentDidMount maps to useEffect(() => { ... }, []) — the empty dependency array is what makes it run once.

WeatherDashboard.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import React, { Component } from 'react';

// Real-world pattern: a dashboard that fetches data on mount
// AND sets up a polling interval to refresh every 60 seconds
class WeatherDashboard extends Component {
  constructor(props) {
    super(props);
    this.state = {
      weatherData: null,
      errorMessage: null,
      lastUpdated: null,
    };
    // We'll store the interval ID so we can clear it on unmount
    this.pollingInterval = null;
  }

  async componentDidMount() {
    // First fetch happens immediately when the component mounts
    await this.loadWeatherData();

    // Then we set up polling — fetch fresh data every 60 seconds
    // Store the interval ID so componentWillUnmount can clean it up
    this.pollingInterval = setInterval(async () => {
      console.log('Polling: refreshing weather data...');
      await this.loadWeatherData();
    }, 60000); // 60,000ms = 60 seconds
  }

  async loadWeatherData() {
    const { cityName } = this.props;

    try {
      // In a real app this would be: fetch(`/api/weather?city=${cityName}`)
      const response = await fetch(
        `https://api.open-meteo.com/v1/forecast?latitude=51.5&longitude=-0.1&current_weather=true`
      );

      if (!response.ok) {
        throw new Error(`API returned status ${response.status}`);
      }

      const data = await response.json();

      // setState here is safe — component is definitely mounted
      this.setState({
        weatherData: data.current_weather,
        errorMessage: null,
        lastUpdated: new Date().toLocaleTimeString(),
      });
    } catch (fetchError) {
      // Always handle errors — never let a rejected promise go silent
      this.setState({
        errorMessage: `Could not load weather: ${fetchError.message}`,
      });
    }
  }

  componentWillUnmount() {
    // CRITICAL: clear the interval when the component unmounts
    // Without this, the interval keeps firing even after the component
    // is gone, causing setState calls on an unmounted component
    if (this.pollingInterval) {
      clearInterval(this.pollingInterval);
      console.log('WeatherDashboard unmounted — polling interval cleared');
    }
  }

  render() {
    const { weatherData, errorMessage, lastUpdated } = this.state;

    if (errorMessage) {
      return <div className="error-banner">{errorMessage}</div>;
    }

    if (!weatherData) {
      return <div className="skeleton-loader">Loading weather...</div>;
    }

    return (
      <div className="weather-card">
        <h3>London Weather</h3>
        <p>Temperature: {weatherData.temperature}°C</p>
        <p>Wind Speed: {weatherData.windspeed} km/h</p>
        <small>Last updated: {lastUpdated}</small>
      </div>
    );
  }
}

export default WeatherDashboard;
Output
// On mount:
Polling: refreshing weather data... (fires after 60s)
Polling: refreshing weather data... (fires after 120s)
// When user navigates away (component unmounts):
WeatherDashboard unmounted — polling interval cleared
// Rendered output:
// Temperature: 14°C
// Wind Speed: 22 km/h
// Last updated: 10:34:05 AM
Watch Out: The Async componentDidMount Trap
Marking componentDidMount as async is fine, but React doesn't await it. If the component unmounts before your async operation finishes, calling setState on the unmounted component will log a warning. The fix: use a mounted flag (this.isMounted = true in didMount, false in willUnmount) or cancel the request with an AbortController.
Production Insight
The async componentDidMount pattern causes a subtle production bug.
React doesn't wait for the async function to complete before allowing unmount.
Rule: wrap async callbacks with a mounted flag check to avoid state-update-on-unmounted warnings.
Key Takeaway
componentDidMount is your guaranteed-safe zone for DOM-dependent operations.
Data fetching, subscriptions, and third-party lib initialisation belong here — not in constructor or render.
If you set it up in didMount, you MUST tear it down in willUnmount.

componentDidUpdate — Responding Intelligently to Change

If componentDidMount is 'do this once on load,' then componentDidUpdate is 'do this whenever something specific changes.' It receives the previous props and previous state as arguments, which is what makes it powerful — you can compare old vs new and react only when it matters.

The single most important rule: always wrap your logic in a conditional comparison. componentDidUpdate fires after every re-render, including renders caused by the setState call inside it. Without a comparison guard, you'll create an infinite loop: state changes → componentDidUpdate fires → setState called → state changes → componentDidUpdate fires...

The comparison pattern (previousProps.someValue !== this.props.someValue) is essentially saying 'only do the expensive work if the relevant input actually changed.' This is the same thinking behind React.memo and useMemo — skip work that doesn't need to happen.

A real-world use case: imagine a data table where the user can select a date range. Every time the date range changes, you need to re-fetch the report. componentDidUpdate lets you watch for exactly that change and respond to it, without rebuilding the entire component.

In hooks, this maps to useEffect with specific dependencies: useEffect(() => { ... }, [dateRange]).

SalesReportTable.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import React, { Component } from 'react';

// Real-world pattern: a report that re-fetches when filters change
// This is the most common use case for componentDidUpdate
class SalesReportTable extends Component {
  constructor(props) {
    super(props);
    this.state = {
      reportRows: [],
      isFetching: false,
      totalRevenue: 0,
    };
  }

  componentDidMount() {
    // Fetch the initial report using the starting prop values
    this.fetchSalesReport(this.props.startDate, this.props.endDate, this.props.regionFilter);
  }

  componentDidUpdate(previousProps) {
    // Check if ANY of the filter props changed
    // We check ALL relevant props in one condition to avoid multiple fetches
    const dateRangeChanged =
      previousProps.startDate !== this.props.startDate ||
      previousProps.endDate !== this.props.endDate;

    const regionChanged = previousProps.regionFilter !== this.props.regionFilter;

    // Only re-fetch if something that affects the report actually changed
    // This guard is what prevents the infinite loop
    if (dateRangeChanged || regionChanged) {
      console.log('Filters changed — fetching updated sales report');
      this.fetchSalesReport(
        this.props.startDate,
        this.props.endDate,
        this.props.regionFilter
      );
    }

    // You can also react to STATE changes, not just prop changes
    // For example: scroll to bottom when new rows are added
    if (previousProps.reportRows !== this.state.reportRows && this.tableRef) {
      this.tableRef.scrollTop = this.tableRef.scrollHeight;
    }
  }

  async fetchSalesReport(startDate, endDate, region) {
    // Set loading state — triggers a re-render but componentDidUpdate
    // won't re-fetch because the props haven't changed
    this.setState({ isFetching: true });

    try {
      // Simulated API response
      const mockRows = [
        { id: 1, product: 'Enterprise License', revenue: 12000, region },
        { id: 2, product: 'Pro Subscription', revenue: 3400, region },
        { id: 3, product: 'Consulting Hours', revenue: 5600, region },
      ];

      const totalRevenue = mockRows.reduce((sum, row) => sum + row.revenue, 0);

      this.setState({
        reportRows: mockRows,
        totalRevenue,
        isFetching: false,
      });
    } catch (error) {
      this.setState({ isFetching: false });
      console.error('Failed to fetch report:', error);
    }
  }

  render() {
    const { reportRows, isFetching, totalRevenue } = this.state;
    const { startDate, endDate, regionFilter } = this.props;

    return (
      <div className="report-container">
        <h3>
          Sales Report: {startDate} to {endDate} ({regionFilter})
        </h3>

        {isFetching ? (
          <p>Refreshing data...</p>
        ) : (
          <table>
            <thead>
              <tr><th>Product</th><th>Revenue</th><th>Region</th></tr>
            </thead>
            <tbody>
              {reportRows.map((row) => (
                <tr key={row.id}>
                  <td>{row.product}</td>
                  <td>${row.revenue.toLocaleString()}</td>
                  <td>{row.region}</td>
                </tr>
              ))}
            </tbody>
            <tfoot>
              <tr><td colSpan="2"><strong>Total: ${totalRevenue.toLocaleString()}</strong></td></tr>
            </tfoot>
          </table>
        )}
      </div>
    );
  }
}

export default SalesReportTable;
Output
// Initial mount:
Fetching sales report for: 2024-01-01 to 2024-01-31 (North America)
// User changes region to 'Europe':
Filters changed — fetching updated sales report
// Rendered table:
// Product | Revenue | Region
// Enterprise License | $12,000 | Europe
// Pro Subscription | $3,400 | Europe
// Consulting Hours | $5,600 | Europe
// Total: $21,000
Pro Tip: Compare Deep Objects with JSON.stringify (Carefully)
If your prop is an object or array, previousProps.filters !== this.props.filters will always be true (different references). For shallow cases, compare specific fields. For complex filters, use JSON.stringify(previousProps.filters) !== JSON.stringify(this.props.filters) — but only if the object is small and stable in structure. For production, reach for a deep-equality library or restructure your state to use primitives.
Production Insight
The infinite loop from missing comparison guard is the most common lifecycle bug we debug.
A setState inside componentDidUpdate without a guard causes re-render → didUpdate → setState → re-render.
Rule: always compare previous and current values before calling setState in componentDidUpdate.
Key Takeaway
componentDidUpdate fires after every re-render — not just the one you care about.
Always wrap your logic with a conditional comparison guard.
Without it, you WILL create an infinite loop that freezes the browser tab.

componentWillUnmount — The Cleanup Phase You Can't Afford to Skip

componentWillUnmount is the most overlooked lifecycle method, and it's the one that causes the most insidious production bugs. It fires right before the component is removed from the DOM — your last chance to clean up anything that isn't managed by React's reconciliation engine.

The stuff you must clean up: timers (setInterval, setTimeout), event listeners on window or document, WebSocket or SignalR connections, third-party library instances, observer subscriptions (ResizeObserver, IntersectionObserver), and in-flight fetch requests. If you created it in componentDidMount and it's not a React-managed DOM node, it belongs in componentWillUnmount.

Here's what happens when you don't clean up: the timer or subscription keeps running, but the component it references is gone. If that callback calls setState, you get the infamous 'Can't perform a React state update on an unmounted component' warning. More importantly, you're leaking memory — the old component tree can't be garbage collected because the timer closure holds a reference to it. In a long-running SPA, components accumulate, memory grows, and eventually the browser tab crashes.

With hooks, the cleanup function returned from useEffect handles this. Every time the effect re-runs, the previous cleanup runs first. When the component unmounts, the final cleanup runs. This co-location of setup and teardown is one of the strongest arguments for functional components.

ChatSubscription.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import React, { Component, createRef } from 'react';

// Real-world scenario: a chat panel that connects to a WebSocket
// on mount and MUST disconnect on unmount to prevent resource leaks
class ChatPanel extends Component {
  constructor(props) {
    super(props);
    this.state = {
      messages: [],
      onlineUsers: 0,
      connectionStatus: 'disconnected',
    };
    this.chatSocket = null;
    this.reconnectTimer = null;
    this.scrollRef = createRef();
  }

  componentDidMount() {
    // Connect to the chat WebSocket
    this.connectToChat(this.props.channelId);

    // Add a resize listener to adjust chat dimensions
    window.addEventListener('resize', this.handleWindowResize);
  }

  componentDidUpdate(prevProps) {
    // If the user switches to a different chat channel,
    // disconnect from old channel and connect to new one
    if (prevProps.channelId !== this.props.channelId) {
      console.log('Channel changed — reconnecting...');
      this.disconnectFromChat();
      this.connectToChat(this.props.channelId);
    }
  }

  componentWillUnmount() {
    // THREE things must be cleaned up:
    // 1. WebSocket connection
    // 2. Reconnect timer
    // 3. Window resize listener
    this.disconnectFromChat();

    // Clear any scheduled reconnect
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
      console.log('Reconnect timer cleared');
    }

    // Remove event listener — using the EXACT same reference as addEventListener
    window.removeEventListener('resize', this.handleWindowResize);
    console.log('ChatPanel unmounted — all resources released');
  }

  // Stored as a class property so we can reference it in both addEventListener
  // and removeEventListener with the exact same function reference
  handleWindowResize = () => {
    console.log('Window resized — adjusting chat dimensions');
    // Adjust chat UI dimensions
  };

  connectToChat(channelId) {
    console.log(`Connecting to chat channel: ${channelId}`);
    this.setState({ connectionStatus: 'connecting' });

    // Simulate a WebSocket connection using setInterval
    // A real implementation would use: new WebSocket(`wss://chat.example.com/${channelId}`)
    this.chatSocket = setInterval(() => {
      const mockMessages = [
        { id: Date.now(), user: 'Alice', text: 'Hey everyone!' },
        { id: Date.now() + 1, user: 'Bob', text: 'Morning!' },
      ];

      // Simulate receiving a new message
      const randomMessage = mockMessages[Math.floor(Math.random() * mockMessages.length)];

      this.setState((prevState) => ({
        messages: [...prevState.messages, { ...randomMessage, timestamp: Date.now() }],
        onlineUsers: Math.floor(Math.random() * 50) + 10,
        connectionStatus: 'connected',
      }));
    }, 4000);

    // Simulate initial connection delay
    setTimeout(() => {
      this.setState({ connectionStatus: 'connected' });
    }, 500);
  }

  disconnectFromChat() {
    if (this.chatSocket) {
      clearInterval(this.chatSocket);
      this.chatSocket = null;
      this.setState({ connectionStatus: 'disconnected' });
      console.log('Disconnected from chat');
    }
  }

  render() {
    const { messages, onlineUsers, connectionStatus } = this.state;

    return (
      <div className="chat-panel" ref={this.scrollRef}>
        <div className="chat-header">
          <span>Online users: {onlineUsers}</span>
          <span className={`connection-badge ${connectionStatus}`}>
            {connectionStatus}
          </span>
        </div>

        <div className="chat-messages">
          {messages.map((msg) => (
            <div key={msg.id} className="message-row">
              <strong>{msg.user}:</strong> {msg.text}
            </div>
          ))}
        </div>
      </div>
    );
  }
}

export default ChatPanel;
Output
// On mount (channelId: 'general'):
Connecting to chat channel: general
Connecting to chat channel: general
Connection established
// After 4s: receives message from Alice
// After 8s: receives message from Bob
// If channelId changes to 'random':
Channel changed — reconnecting...
Disconnected from chat
Connecting to chat channel: random
Connection established
// On unmount:
Disconnected from chat
Reconnect timer cleared
Window resize listener removed
ChatPanel unmounted — all resources released
The Event Listener Trap
window.addEventListener and window.removeEventListener must receive the EXACT same function reference. If you use an anonymous arrow function in addEventListener, you can't reference it in removeEventListener — the listener never gets removed. Always assign the handler to a class property or a named variable.
Production Insight
Memory leaks from missing cleanup compound over time in SPAs with nested routing.
Each navigation that forgets to unsubscribe leaves a dead component tree in memory.
Rule: use the Chrome DevTools Memory profiler — take a heap snapshot, navigate away, take another snapshot. If memory doesn't drop, you have a leak.
Key Takeaway
componentWillUnmount is your last chance to prevent a memory leak.
Every timer, listener, and subscription created in didMount MUST be destroyed here.
Skip it and you'll accumulate dead components until the browser tab crashes.

Hooks vs Class Lifecycle Methods — The Modern Equivalent

Class lifecycle methods aren't going anywhere — you'll encounter them in every legacy codebase. But since React 16.8, hooks have become the modern way to handle the same concerns in functional components. Knowing both, and how they map to each other, is what separates a solid React developer from one who only knows the happy path.

The key mental shift: useEffect doesn't map one-to-one to a single lifecycle method. It can replicate all three — mounting, updating, and unmounting — depending on how you configure it. The dependency array is the control mechanism.

An empty array [] means 'run once after mount' — that's componentDidMount. A populated array [userId] means 'run after mount AND after any render where userId changed' — that's componentDidMount plus componentDidUpdate with a comparison guard built in. No array at all means 'run after every render' — dangerous, but it exists. And the return value of useEffect is the cleanup function — that's componentWillUnmount.

The functional approach collapses three methods into one concept, which is more composable. But the class approach makes the phases more explicit, which can actually be easier to reason about when learning.

NotificationSubscription.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import React, { Component, useState, useEffect } from 'react';

// ─────────────────────────────────────────────────────────
// CLASS COMPONENT VERSION — explicit lifecycle methods
// ─────────────────────────────────────────────────────────
class NotificationPanelClass extends Component {
  constructor(props) {
    super(props);
    this.state = { notifications: [], connectionStatus: 'disconnected' };
    this.socketConnection = null;
  }

  componentDidMount() {
    // Set up a WebSocket-like subscription when the component mounts
    this.connectToNotificationStream(this.props.userId);
  }

  componentDidUpdate(previousProps) {
    // If the user changes (e.g. admin views another user's notifications),
    // disconnect the old stream and connect to the new one
    if (previousProps.userId !== this.props.userId) {
      this.disconnectFromStream();
      this.connectToNotificationStream(this.props.userId);
    }
  }

  componentWillUnmount() {
    // Always close the connection when the panel is hidden/removed
    this.disconnectFromStream();
  }

  connectToNotificationStream(userId) {
    console.log(`Connecting to stream for user: ${userId}`);
    this.setState({ connectionStatus: 'connected' });

    // Simulating a real-time notification stream
    this.socketConnection = setInterval(() => {
      this.setState((previousState) => ({
        notifications: [
          ...previousState.notifications,
          { id: Date.now(), message: `New alert for user ${userId}` },
        ],
      }));
    }, 3000);
  }

  disconnectFromStream() {
    if (this.socketConnection) {
      clearInterval(this.socketConnection);
      this.socketConnection = null;
      this.setState({ connectionStatus: 'disconnected' });
      console.log('Disconnected from notification stream');
    }
  }

  render() {
    const { notifications, connectionStatus } = this.state;
    return (
      <div>
        <span>Status: {connectionStatus}</span>
        <ul>{notifications.map((n) => <li key={n.id}>{n.message}</li>)}</ul>
      </div>
    );
  }
}

// ─────────────────────────────────────────────────────────
// FUNCTIONAL COMPONENT VERSION — same logic, hooks style
// Notice how useEffect with a cleanup return collapses
// componentDidMount + componentDidUpdate + componentWillUnmount
// ─────────────────────────────────────────────────────────
function NotificationPanelHooks({ userId }) {
  const [notifications, setNotifications] = useState([]);
  const [connectionStatus, setConnectionStatus] = useState('disconnected');

  useEffect(() => {
    // This block = componentDidMount + componentDidUpdate (when userId changes)
    console.log(`Connecting to stream for user: ${userId}`);
    setConnectionStatus('connected');

    const streamInterval = setInterval(() => {
      setNotifications((previousNotifications) => [
        ...previousNotifications,
        { id: Date.now(), message: `New alert for user ${userId}` },
      ]);
    }, 3000);

    // The return function = componentWillUnmount
    // React also calls this cleanup BEFORE re-running the effect
    // when userId changes — so we get automatic reconnection logic
    return () => {
      clearInterval(streamInterval);
      setConnectionStatus('disconnected');
      console.log('Disconnected from notification stream');
    };

  }, [userId]); // Only re-run this effect when userId changes

  return (
    <div>
      <span>Status: {connectionStatus}</span>
      <ul>{notifications.map((n) => <li key={n.id}>{n.message}</li>)}</ul>
    </div>
  );
}

export { NotificationPanelClass, NotificationPanelHooks };
Output
// Both components produce identical behaviour:
// On mount (userId = 'user_42'):
Connecting to stream for user: user_42
// After 3s: renders 'New alert for user user_42'
// After 6s: renders second alert
// If userId prop changes to 'user_99':
Disconnected from notification stream
Connecting to stream for user: user_99
// On unmount:
Disconnected from notification stream
Interview Gold:
When asked 'how does useEffect compare to lifecycle methods?', don't just say 'it replaces them.' Say this: 'useEffect unifies all three lifecycle phases into one API. The dependency array controls when it runs, and the return function handles cleanup. The real power is that it co-locates related setup and teardown logic, which class components force you to split across three separate methods.'
Production Insight
The useEffect dependency array is the most common source of bugs in functional React.
Missing a dependency causes stale closures — the effect runs but reads old values.
Rule: the exhaustive-deps ESLint rule is not optional. Turn it on and treat its warnings as errors.
Key Takeaway
useEffect collapses mount, update, and unmount into one composable API.
The dependency array controls when it runs — empty for mount-only, populated for selective update.
The cleanup return function is your componentWillUnmount — co-located with the setup it tears down.

Class Lifecycle Methods vs Hooks — Quick Reference Table

Use this table to quickly map class lifecycle methods to their hooks equivalent and understand when each is used.

Class Lifecycle MethodPhaseHooks EquivalentWhen to Use
constructorMountinguseState initialiserSet initial state and bind methods. Avoid side effects.
static getDerivedStateFromPropsMount/UpdateuseState + useEffect with dep, or key propRarely needed. Used when state must be synced with props before render (e.g., form reset on user change).
shouldComponentUpdateUpdateReact.memo (for props), useMemo, useCallbackPerformance optimisation. Prevent re-render when props/state haven't changed.
renderAllFunction bodyPure function that returns JSX. No side effects.
getSnapshotBeforeUpdateUpdateuseLayoutEffectCapture DOM values (scroll position, cursor) before update. Used with componentDidUpdate.
componentDidMountMountinguseEffect(() => {}, [])Data fetching, subscriptions, DOM manipulation after component is visible.
componentDidUpdateUpdateuseEffect(() => {}, [dep])React to prop/state changes with side effects. Always compare prev vs current.
componentWillUnmountUnmountreturn () => { } in useEffectCleanup timers, subscriptions, event listeners, cancel async requests.
componentDidCatchError Handling(No hook equivalent)Log errors in error boundary component.
static getDerivedStateFromErrorError Handling(No hook equivalent)Set hasError state to show fallback UI in error boundary.

Key takeaway: Hooks consolidate lifecycle logic into one API with dependency arrays, but error handling still requires class components. Use this table as a cheat sheet for migrating class components to hooks or for interview preparation.

Interview Relevance
Expect questions about mapping lifecycle methods to useEffect. Knowing this table cold demonstrates you understand both paradigms. Emphasise that error boundaries (componentDidCatch) have no hooks equivalent — that's a common interview trap.
Production Insight
When migrating a large class codebase to hooks, use this table as a systematic guide. For every lifecycle method, identify its replacement and test thoroughly. The most common migration bugs come from missing dependencies in useEffect or forgetting to return a cleanup function.
Key Takeaway
This table maps every class lifecycle method to its functional equivalent. Use it to bridge class and hooks knowledge, migrate codebases, and ace interview questions.

Rarely Used Lifecycle Methods — getDerivedStateFromProps, shouldComponentUpdate, and getSnapshotBeforeUpdate

Most React applications never need these lifecycle methods. They exist for edge cases where the standard state-and-props flow isn't sufficient. But when you need them — form validation against props, performance optimisations, or reading DOM values right before a change — knowing they exist will save hours of workarounds.

getDerivedStateFromProps is a static method that runs before every render, both on mount and on update. It receives nextProps and prevState, and returns an object to update state, or null for no update. The classic use case: a form that resets when a user ID prop changes — the new user should see blank fields, not the previous user's data. Before React 16.3, you'd have used componentWillReceiveProps (now deprecated). This method is the replacement. It's static, so you cannot access 'this' or call other instance methods — it's purely for state derivation.

shouldComponentUpdate is the performance escape hatch. It receives nextProps and nextState, and returns true (default) to allow a re-render, or false to skip it. Use this when a component re-renders too often on props or state changes that don't affect its output. A pure functional component with React.memo gives you this optimization automatically; for class components, you implement shouldComponentUpdate manually, or extend PureComponent which does a shallow prop and state comparison for you.

getSnapshotBeforeUpdate runs right before React applies the DOM changes from a render. It receives prevProps and prevState, and returns a value that gets passed as the third argument to componentDidUpdate. The classic use case: preserving scroll position when adding new messages to a chat — you capture the scroll height before the DOM updates, then restore it after the update completes. Without this method, the scroll position jumps to the bottom when new messages arrive, breaking the user's reading position.

These methods are rarely used by design. If you find yourself reaching for them frequently, reconsider whether your component structure is correct. They are tools of last resort, not everyday patterns.

RareLifecycleMethods.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import React, { Component } from 'react';

// ============================================================
// EXAMPLE 1: getDerivedStateFromProps — Reset form when userId changes
// ============================================================
class UserProfileForm extends Component {
  constructor(props) {
    super(props);
    this.state = {
      name: '',
      email: '',
      prevUserId: props.userId,  // Track the last userId that caused a reset
    };
  }

  static getDerivedStateFromProps(nextProps, prevState) {
    // If the userId prop changed, reset the form fields
    if (nextProps.userId !== prevState.prevUserId) {
      return {
        name: '',
        email: '',
        prevUserId: nextProps.userId,
      };
    }
    // No state update needed
    return null;
  }

  handleChange = (field, value) => {
    this.setState({ [field]: value });
  };

  render() {
    const { name, email } = this.state;
    const { userId } = this.props;

    return (
      <div>
        <h3>Editing User: {userId}</h3>
        <input
          placeholder="Name"
          value={name}
          onChange={(e) => this.handleChange('name', e.target.value)}
        />
        <input
          placeholder="Email"
          value={email}
          onChange={(e) => this.handleChange('email', e.target.value)}
        />
      </div>
    );
  }
}

// ============================================================
// EXAMPLE 2: shouldComponentUpdate — Performance optimization
// ============================================================
class ExpensiveChart extends Component {
  // Only re-render if the data array reference or color actually changed
  // Without this, every parent re-render would trigger a full chart re-draw
  shouldComponentUpdate(nextProps, nextState) {
    // Compare data references (assume immutable data)
    if (this.props.data !== nextProps.data) return true;
    // Compare color string
    if (this.props.color !== nextProps.color) return true;
    // No changes that affect the chart
    return false;
  }

  render() {
    const { data, color } = this.props;
    console.log('ExpensiveChart re-rendered');
    return <div style={{ color }}>Rendering chart with {data.length} points</div>;
  }
}

// ============================================================
// EXAMPLE 3: getSnapshotBeforeUpdate — Preserve scroll position
// ============================================================
class ChatMessageList extends Component {
  constructor(props) {
    super(props);
    this.messageContainerRef = React.createRef();
    this.state = { messages: [] };
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // Capture the scroll height before the DOM updates
    const container = this.messageContainerRef.current;
    if (container) {
      return {
        scrollHeightBefore: container.scrollHeight,
        scrollTopBefore: container.scrollTop,
      };
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    if (snapshot && this.messageContainerRef.current) {
      const container = this.messageContainerRef.current;
      const newScrollHeight = container.scrollHeight;
      const heightIncrease = newScrollHeight - snapshot.scrollHeightBefore;

      // If the user was scrolled near the bottom, keep them at the bottom
      const wasNearBottom = snapshot.scrollTopBefore + container.clientHeight >=
        snapshot.scrollHeightBefore - 20;

      if (wasNearBottom) {
        container.scrollTop = newScrollHeight;
      }
    }
  }

  addMessage = () => {
    this.setState((prev) => ({
      messages: [
        ...prev.messages,
        `Message at ${new Date().toLocaleTimeString()}`,
      ],
    }));
  };

  render() {
    return (
      <div>
        <div
          ref={this.messageContainerRef}
          style={{ height: '200px', overflowY: 'auto', border: '1px solid #ccc' }}
        >
          {this.state.messages.map((msg, i) => (
            <div key={i}>{msg}</div>
          ))}
        </div>
        <button onClick={this.addMessage}>Add Message</button>
      </div>
    );
  }
}

export { UserProfileForm, ExpensiveChart, ChatMessageList };
Output
// getDerivedStateFromProps: When userId changes from 1 to 2
// Form fields reset to empty strings automatically
// shouldComponentUpdate: Parent re-renders 10 times, but data prop unchanged
// ExpensiveChart only re-renders once → console.log appears once
// getSnapshotBeforeUpdate: When new message added while scrolled to bottom
// Scroll position stays at bottom automatically
When to Use These Methods
| Method | Use Case | Modern Alternative | |--------|----------|-------------------| | getDerivedStateFromProps | Reset state when prop changes | Use a key prop on the component, or use the useEffect hook with cleanup | | shouldComponentUpdate | Performance optimization for class components | React.memo for functional components, or useMemo/useCallback for values | | getSnapshotBeforeUpdate | Read DOM before update (scroll position, cursor) | useLayoutEffect can capture scroll position before the browser paints |
Production Insight
A team used shouldComponentUpdate to skip re-renders of a 500-row table when irrelevant props changed. Without it, every keystroke in a search box above the table caused 500 component re-renders and a noticeable UI lag. After implementing shouldComponentUpdate (or React.memo on the functional version), the table re-rendered only when its actual data changed. Rule: use performance optimisations only when you measure a problem — premature optimization adds complexity without measurable benefit.
Key Takeaway
getDerivedStateFromProps resets state when props change — use it for forms that must clear on new data. shouldComponentUpdate prevents unnecessary re-renders — use it only after measuring a performance problem. getSnapshotBeforeUpdate captures DOM values before an update — use it for scroll position preservation and cursor management.

Visual Lifecycle Diagram — Mounting, Updating, and Unmounting Flow

Understanding the lifecycle order visually helps you predict when your code will run. The diagram below shows the complete sequence from component creation to destruction, including the rarely used methods.

Mounting Phase (component birth): constructor() → static getDerivedStateFromProps() → render() → componentDidMount()

The constructor runs first, setting initial state. Then getDerivedStateFromProps gives you a chance to update state based on initial props. render computes the virtual DOM, and React updates the real DOM. Finally, componentDidMount fires — the component is now fully inserted, ready for side effects.

Updating Phase (props or state change): static getDerivedStateFromProps() → shouldComponentUpdate() → render() → getSnapshotBeforeUpdate() → componentDidUpdate()

When new props arrive or setState is called, getDerivedStateFromProps runs first to derive state from the new props. shouldComponentUpdate decides whether to continue (performance optimization). render computes the new virtual DOM and React updates the actual DOM. getSnapshotBeforeUpdate captures DOM values before the update (scroll positions, cursor locations). Finally, componentDidUpdate runs after the DOM is updated.

Unmounting Phase (component death): componentWillUnmount()

One method runs right before removal. This is your cleanup opportunity — timers, subscriptions, event listeners.

Error Handling Phase (when render fails): static getDerivedStateFromError() → componentDidCatch()

When an error occurs during render, getDerivedStateFromError runs first to update state (showing a fallback UI), then componentDidCatch logs the error to an external service.

lifecycle-diagram.txtTEXT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
╔═══════════════════════════════════════════════════════════════════════════════════╗
║                          REACT CLASS COMPONENT LIFECYCLE                            ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║                                                                                   ║
║  ┌─────────────────────────────────────────────────────────────────────────────┐   ║
║  │                           MOUNTING PHASE                                    │   ║
║  │  ┌──────────────┐    ┌──────────────────────┐    ┌────────┐    ┌───────────┐│   ║
║  │  │ constructor  │ -> │ getDerivedStateFrom  │ -> │ render │ -> │component  ││   ║
║  │  │              │    │       Props          │    │        │    │DidMount   ││   ║
║  │  └──────────────┘    └──────────────────────┘    └────────┘    └───────────┘│   ║
║  │       │                       │                     │               │       │   ║
║  │       │                  (rarely used)          (pure)          (side       │   ║
║  │       │                                            │           effects)     │   ║
║  │       └──────────────────────┬─────────────────────┘               │       │   ║
║  │                              │                                     │       │   ║
║  │                              ▼                                     ▼       │   ║
║  │  ┌─────────────────────────────────────────────────────────────────────┐   │   ║
║  │  │                         UPDATING PHASE                             │   │   ║
║  │  │                                                                     │   │   ║
║  │  │   props change   OR   setState()                                    │   │   ║
║  │  │         │                    │                                      │   │   ║
║  │  │         └──────────┬─────────┘                                      │   │   ║
║  │  │                    ▼                                                │   │   ║
║  │  │  ┌──────────────────────────────────────────────────────────────┐  │   │   ║
║  │  │  │          getDerivedStateFromProps()                          │  │   │   ║
║  │  │  │         (update state from new props — rare)                 │  │   │   ║
║  │  │  └────────────────────────────┬─────────────────────────────────┘  │   │   ║
║  │  │                               ▼                                     │   │   ║
║  │  │  ┌──────────────────────────────────────────────────────────────┐  │   │   ║
║  │  │  │          shouldComponentUpdate()                             │  │   │   ║
║  │  │  │      (performance — return false to skip render)             │  │   │   ║
║  │  │  └────────────────────────────┬─────────────────────────────────┘  │   │   ║
║  │  │                               ▼                                     │   │   ║
║  │  │  ┌──────────────────────────────────────────────────────────────┐  │   │   ║
║  │  │  │                     render()                                 │  │   │   ║
║  │  │  │            (compute new virtual DOM — pure)                  │  │   │   ║
║  │  │  └────────────────────────────┬─────────────────────────────────┘  │   │   ║
║  │  │                               ▼                                     │   │   ║
║  │  │  ┌──────────────────────────────────────────────────────────────┐  │   │   ║
║  │  │  │        getSnapshotBeforeUpdate(prevProps, prevState)         │  │   │   ║
║  │  │  │   (capture DOM values before update — scroll position)       │  │   │   ║
║  │  │  └────────────────────────────┬─────────────────────────────────┘  │   │   ║
║  │  │                               │                                     │   │   ║
║  │  │        [React updates the real DOM]                                 │   │   ║
║  │  │                               │                                     │   │   ║
║  │  │                               ▼                                     │   │   ║
║  │  │  ┌──────────────────────────────────────────────────────────────┐  │   │   ║
║  │  │  │     componentDidUpdate(prevProps, prevState, snapshot)       │  │   │   ║
║  │  │  │          (side effects based on DOM changes)                 │  │   │   ║
║  │  │  └──────────────────────────────────────────────────────────────┘  │   │   ║
║  │  │                               │                                     │   │   ║
║  │  └───────────────────────────────┬─────────────────────────────────────┘   │   ║
║  │                                  │                                           │   ║
║  │                                  ▼                                           │   ║
║  │  ┌─────────────────────────────────────────────────────────────────────┐   │   ║
║  │  │                        UNMOUNTING PHASE                              │   │   ║
║  │  │                                                                      │   │   ║
║  │  │            ┌──────────────────────────────────────┐                 │   │   ║
║  │  │            │       componentWillUnmount()         │                 │   │   ║
║  │  │            │    (clean up timers, subscriptions)  │                 │   │   ║
║  │  │            └──────────────────────────────────────┘                 │   │   ║
║  │  └─────────────────────────────────────────────────────────────────────┘   │   ║
║  │                                                                             │   ║
║  └─────────────────────────────────────────────────────────────────────────────┘   ║
║                                                                                   ║
║  ╔═════════════════════════════════════════════════════════════════════════════╗   ║
║  ║                         ERROR HANDLING PHASE                                ║   ║
║  ║                                                                             ║   ║
║  ║      (error thrown during render or lifecycle method)                       ║   ║
║  ║                              │                                              ║   ║
║  ║                              ▼                                              ║   ║
║  ║      ┌──────────────────────────────────────────────────────────────┐       ║   ║
║  ║      │      static getDerivedStateFromError(error)                  │       ║   ║
║  ║      │           (update state to show fallback UI)                 │       ║   ║
║  ║      └────────────────────────────┬─────────────────────────────────┘       ║   ║
║  ║                                   │                                          ║   ║
║  ║                                   ▼                                          ║   ║
║  ║      ┌──────────────────────────────────────────────────────────────┐       ║   ║
║  ║      │           componentDidCatch(error, errorInfo)                │       ║   ║
║  ║      │              (log error to external service)                 │       ║   ║
║  ║      └──────────────────────────────────────────────────────────────┘       ║   ║
║  ╚═════════════════════════════════════════════════════════════════════════════╝   ║
║                                                                                   ║
║  ┌─────────────────────────────────────────────────────────────────────────┐     ║
║  │                    HOOKS EQUIVALENT QUICK REFERENCE                      │     ║
║  ├─────────────────────────────────────────────────────────────────────────┤     ║
║  │  componentDidMount           →  useEffect(() => {}, [])                  │     ║
║  │  componentDidUpdate          →  useEffect(() => {}, [dep])              │     ║
║  │  componentWillUnmount        →  return () => {} inside useEffect         │     ║
║  │  shouldComponentUpdate       →  React.memo() or useMemo()                │     ║
║  │  getDerivedStateFromProps    →  useState + useEffect with dep, or a key  │     ║
║  │  getSnapshotBeforeUpdate     →  useLayoutEffect()                        │     ║
║  │  componentDidCatch           →  No hook equivalent — use ErrorBoundary   │     ║
║  └─────────────────────────────────────────────────────────────────────────┘     ║
║                                                                                   ║
╚═══════════════════════════════════════════════════════════════════════════════════╝
Memory Aid — The Lifecycle Dance:
Constructor creates → Derive sets → Render describes → Mount appears → Update changes → Derive again → Should decide → Render describes → Snapshot captures → Update responds → Unmount cleans
Production Insight
A team spent three hours debugging why their analytics event wasn't firing on page view. They put the tracking call in constructor, which ran before React was ready. Moving it to componentDidMount fixed the issue immediately. Rule: side effects (API calls, analytics, subscriptions) belong in componentDidMount, not constructor or render. The lifecycle diagram makes this obvious at a glance.
Key Takeaway
Mount: constructor → getDerivedStateFromProps → render → componentDidMount. Update: getDerivedStateFromProps → shouldComponentUpdate → render → getSnapshotBeforeUpdate → componentDidUpdate. Unmount: componentWillUnmount. Error: getDerivedStateFromError → componentDidCatch.
React Class Component Lifecycle Flow
falsetrueConstructorgetDerivedStateFromPropsRendercomponentDidMountWait for Props/State ChangegetDerivedStateFromPropsshouldComponentUpdateRendergetSnapshotBeforeUpdatecomponentDidUpdatecomponentWillUnmountUnmounted

Error Handling Lifecycle Methods — componentDidCatch and getDerivedStateFromError

By default, when a JavaScript error occurs inside a React component during rendering, the entire component tree unmounts and you see a blank white screen. Error boundaries are components that catch these errors, log them, and display a fallback UI instead of crashing the whole application.

componentDidCatch is a lifecycle method that catches errors in the render phase of any component below it. It receives the error object and an info object containing the component stack trace. Use it to log errors to an external service like Sentry or LogRocket.

getDerivedStateFromError is a static method called when an error is caught. It receives the error and must return an object to update state. This is used to render a fallback UI — you set hasError: true, and your render method shows an error message instead of the crashed component tree.

The critical nuance: error boundaries only catch errors during rendering, in lifecycle methods, and in constructors of the whole tree below them. They DO NOT catch errors in event handlers, asynchronous code (setTimeout, fetch), or errors thrown in the error boundary itself. For event handlers, use try/catch and local error state instead.

Error boundaries are class components only. There is no hook equivalent — you must write a class component for an error boundary, even if the rest of your codebase uses functional components.

Place error boundaries strategically, not everywhere. Wrap major UI sections (header, main content, sidebar) so a failure in one doesn't nuke the whole page. A good pattern is one error boundary per route or per major feature module.

ErrorBoundary.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import React, { Component } from 'react';

// ============================================================
// ERROR BOUNDARY COMPONENT — Catches render errors
// ============================================================
class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = {
      hasError: false,
      error: null,
      errorInfo: null,
    };
  }

  // static method 1: Update state to show fallback UI
  static getDerivedStateFromError(error) {
    // Return an object to update state
    return { hasError: true };
  }

  // instance method 2: Log the error to an external service
  componentDidCatch(error, errorInfo) {
    // Log error to your monitoring service (Sentry, LogRocket, etc.)
    console.error('ErrorBoundary caught an error:', error, errorInfo);

    this.setState({
      error: error,
      errorInfo: errorInfo,
    });

    // In production, send to your error tracking service
    // window.Sentry.captureException(error, { extra: errorInfo });
  }

  render() {
    if (this.state.hasError) {
      // Fallback UI — show something friendly
      return (
        <div className="error-fallback">
          <h2>Something went wrong</h2>
          <details style={{ whiteSpace: 'pre-wrap' }}>
            <summary>Technical details (only in development)</summary>
            {this.state.error && this.state.error.toString()}
            <br />
            {this.state.errorInfo && this.state.errorInfo.componentStack}
          </details>
          <button onClick={() => window.location.reload()}>Reload page</button>
        </div>
      );
    }

    // No error — render children normally
    return this.props.children;
  }
}

// ============================================================
// COMPONENT THAT THROWS ON PURPOSE (for testing)
// ============================================================
const BuggyCounter = () => {
  const [count, setCount] = React.useState(0);

  if (count === 5) {
    // This error will be caught by the closest error boundary
    throw new Error('Intentional crash at count = 5');
  }

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

// ============================================================
// USING THE ERROR BOUNDARY
// ============================================================
const App = () => {
  return (
    <div>
      <h1>My App</h1>

      {/* Wrap risky component in error boundary */}
      <ErrorBoundary>
        <BuggyCounter />
      </ErrorBoundary>

      {/* This component continues working even if BuggyCounter crashes */}
      <div>This text remains visible if the counter crashes above</div>
    </div>
  );
};

export { ErrorBoundary, BuggyCounter, App };

// ============================================================
// WHAT ERROR BOUNDARIES DO NOT CATCH
// ============================================================
// ❌ Event handlers (use try/catch + local state)
// const handleClick = () => {
//   try {
//     riskyOperation();
//   } catch (error) {
//     setHasError(true);  // local error state
//   }
// };
//
// ❌ Asynchronous code (setTimeout, requestAnimationFrame)
// ❌ Server-side rendering errors
// ❌ Errors thrown in the error boundary itself
Output
// Normal operation:
// Count: 0 → Increment → Count: 1 → ... → Count: 4
// When count reaches 5:
// ErrorBoundary catches the throw
// componentDidCatch logs: 'ErrorBoundary caught an error: Intentional crash at count = 5'
// getDerivedStateFromError sets hasError: true
// Fallback UI renders: 'Something went wrong'
// The rest of the app continues functioning
Critical: What Error Boundaries Do NOT Catch
Error boundaries catch only rendering errors. They do NOT catch errors in: 1. Event handlers (onClick, onChange) — use try/catch and local error state 2. Asynchronous code (setTimeout, requestAnimationFrame, fetch promises) 3. Server-side rendering (errors there crash the Node process, not the boundary) 4. Errors thrown in the error boundary component itself For event handlers, the pattern is: try { riskyAction(); } catch (err) { setError(err); }
Production Insight
A production checkout page crashed to a white screen because a product image component threw an error when an image URL was malformed. The team didn't have an error boundary around the product list. One bad image URL took down the entire checkout flow. Adding an error boundary around the product grid component allowed the rest of the page to render, showing an error message in the product section while the checkout button remained usable. Rule: place error boundaries at the major UI section boundaries — header, main content, sidebar, footer — so failures are isolated to the broken component only.
Key Takeaway
Error boundaries prevent one component's render error from crashing the whole page. Use getDerivedStateFromError to show a fallback UI and componentDidCatch to log errors. Error boundaries are class components only — no hook equivalent exists.

The Constructor is a Trap — Here's What to Actually Do in It

Most junior devs treat the constructor like a kitchen sink. They initialize state, bind methods, fetch data, set up subscriptions. Wrong on all counts.

The constructor is for exactly two things: initializing local state from props and binding event handlers. That's it. If you're doing side effects in a constructor, you're fighting React's design.

Here's the hard truth: the constructor runs before the component mounts. There's no DOM. No DOM access. No way to interact with the real UI. Data fetching here is pointless — you'll have to handle a loading state anyway. Subscriptions here will leak because you haven't setup cleanup yet.

Use it. But use it sparingly. If you're using hooks, skip it entirely — functional components don't have constructors, and that's a feature, not a bug.

ConstructorPitfall.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// io.thecodeforge — javascript tutorial

class UserProfile extends React.Component {
  constructor(props) {
    super(props);
    // Only do this:
    this.state = {
      user: null,
      loading: true
    };
    // Never do this:
    // fetch('/api/users').then(...) ← Wrong!
    // this.interval = setInterval(...) ← Wrong!
  }

  componentDidMount() {
    // Do side effects here
    this.fetchUser();
  }

  render() {
    return <div>{this.state.loading ? 'Loading...' : this.state.user.name}</div>;
  }
}

// With hooks — no constructor at all
function UserProfile() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchUser()
      .then(data => setUser(data))
      .finally(() => setLoading(false));
  }, []);

  return <div>{loading ? 'Loading...' : user.name}</div>;
}
Output
// No output — constructor just initializes state
Senior Shortcut:
If your constructor is longer than 5 lines, you're doing it wrong. Move side effects to componentDidMount or useEffect.
Key Takeaway
Constructors initialize state and bind event handlers — nothing more. Side effects in constructors are a production incident waiting to happen.

Render is a Pure Function — Stop Breaking That Contract

The render method is the heart of every class component. It's called on mount and every update. And it must be pure — no side effects, no mutations, no state changes.

Here's why: React calls render to figure out what the DOM should look like. Then it diffs that against the previous state. If your render method does something — like fetching data, setting timers, or modifying global state — you'll get inconsistent results and debugging nightmares.

I've seen production code where render was making network requests. The app worked on fast connections. On slow ones, it broke in ways we couldn't reproduce. Took a week to find the bug.

The rule is simple: render should return a React element. That's it. No API calls, no subscriptions, no side effects. If you need to do something based on props or state changes, use componentDidUpdate or useEffect.

This is the contract React gives you. Break it, and your code will be the reason someone's pager goes off at 3 AM.

RenderPurity.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// io.thecodeforge — javascript tutorial

class SearchResults extends React.Component {
  render() {
    // ❌ WRONG: Side effect inside render
    // fetch('/api/search?q=' + this.props.query)
    //   .then(res => res.json())
    //
    // ✅ CORRECT: Just return JSX
    return (
      <div>
        {this.props.results.map(item => (
          <ResultCard key={item.id} data={item} />
        ))}
      </div>
    );
  }

  componentDidUpdate(prevProps) {
    // ❌ Do API calls here or in useEffect
    if (prevProps.query !== this.props.query) {
      fetch('/api/search?q=' + this.props.query)
        .then(res => res.json())
        .then(data => this.setState({ results: data }));
    }
  }
}

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

  useEffect(() => {
    fetch('/api/search?q=' + query)
      .then(res => res.json())
      .then(setResults);
  }, [query]);

  return (
    <div>
      {results.map(item => (
        <ResultCard key={item.id} data={item} />
      ))}
    </div>
  );
}
Output
// No output — render returns JSX only
Production Trap:
Never mutate state, make API calls, or set timers inside render. Use componentDidUpdate or useEffect for side effects. Your code will thank you.
Key Takeaway
Render is a pure function that returns JSX. Side effects belong in lifecycle methods or hooks — not inside render.

The Unmount You Forget Will Haunt You — Memory Leaks 101

componentWillUnmount is where components go to die. But most developers ignore it until their app leaks memory like a sieve.

Here's what happens: you mount a component, it fetches data, sets up a WebSocket connection, subscribes to a store. The component is never used again — user navigates away, closes the tab, whatever. But your subscriptions, timers, and event listeners are still running.

A memory leak is when your app holds onto references it doesn't need. A React component that's unmounted but still holds subscriptions is a classic example. Over time, your app consumes more and more memory until the browser kills it.

I've seen production apps that froze after 20 minutes of normal use because someone forgot to clear an interval in componentWillUnmount. The fix was one line of code: clearInterval(this.interval).

If you use hooks, useEffect's cleanup function handles this. That's not optional — you must return a cleanup function for every effect that creates subscriptions, timers, or event listeners.

Your app's performance depends on you remembering this. Forget, and your users will wonder why their browser tabs crash randomly.

UnmountCleanup.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// io.thecodeforge — javascript tutorial

class RealtimeChart extends React.Component {
  componentDidMount() {
    this.ws = new WebSocket('wss://api.example.com/stream');
    this.ws.onmessage = (event) => {
      this.setState({ data: JSON.parse(event.data) });
    };
    this.interval = setInterval(() => {
      // heartbeat
      this.ws.send('ping');
    }, 30000);
  }

  componentWillUnmount() {
    // 🚨 MUST DO: Clean up everything
    this.ws.close();
    clearInterval(this.interval);
  }

  render() {
    return <ChartComponent data={this.state.data} />;
  }
}

// With hooks
function RealtimeChart() {
  const [data, setData] = useState(null);

  useEffect(() => {
    const ws = new WebSocket('wss://api.example.com/stream');
    ws.onmessage = (event) => setData(JSON.parse(event.data));
    
    const interval = setInterval(() => ws.send('ping'), 30000);

    // Cleanup function runs on unmount
    return () => {
      ws.close();
      clearInterval(interval);
    };
  }, []);

  return <ChartComponent data={data} />;
}

// ⚠️ Output if cleanup is missing:
// WebSocket.readyState: 3 (CLOSED)
// Memory keeps growing every interval tick
Output
// Memory leak without cleanup:
// After 100 ticks: 150MB memory
// After 30 minutes: Browser tab freezes
// After cleanup: Memory stable at 20MB
Senior Shortcut:
Every useEffect that creates subscriptions, timers, or event listeners MUST return a cleanup function. No exceptions.
Key Takeaway
Unmount without cleanup is a memory leak. Always close WebSockets, clear intervals, and remove event listeners in componentWillUnmount or useEffect's cleanup.

Why This Guide?

Most React lifecycle content teaches you what methods exist. This guide exists because knowing method names won't save you from production bugs. The real cost of lifecycle mismanagement shows up as memory leaks, stale closures, and silent data corruption. We've seen teams ship features that work locally but crash under load because componentWillUnmount wasn't cleaning up WebSocket connections. This guide prioritizes behavioral contracts over API trivia. Each section answers: what does the framework expect from me at this point, and what breaks if I violate that contract? You will learn why setState inside componentDidUpdate without a guard causes infinite loops, why mounting subscriptions without tracking cleanup leads to duplicate event handlers, and why React calls render twice in development. This isn't a reference — it's a survival manual for components that stay alive across re-renders without corrupting state or leaking resources.

WhyGuideExample.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// io.thecodeforge — javascript tutorial

class SubscriptionManager extends React.Component {
  // Contract: mount -> setup; unmount -> teardown
  componentDidMount() {
    this.interval = setInterval(this.syncData, 1000);
  }

  // Violation: no cleanup = memory leak per mount+unmount cycle
  componentWillUnmount() {
    clearInterval(this.interval);
  }

  syncData = () => {
    console.log('fetching latest...');
  };
}

// The bug: missing clearInterval doubles calls after re-mount
// UseEffect equivalent with cleanup solves it deterministically
Output
// No console output — this pattern runs silently.
// Without cleanup, intervals stack and never stop.
// Verify with: React DevTools > Profiler > unmount + mount
Production Trap:
Never assume single instance. React StrictMode double-invokes lifecycle methods in development — bugs hidden in production appear when components mount multiple times.
Key Takeaway
This guide exists because lifecycle bugs are silent until production: every mount must have a matching unmount.

Benefits of Custom Hooks

Custom hooks exist to eliminate lifecycle duplication across components. Without them, every component that needs window resize events writes the same componentDidMount handler, the same addEventListener, and the same componentWillUnmount cleanup. Custom hooks extract this triplicate lifecycle contract into a single function. They enforce the why: every effect must leave the system in the same state it found it. A useWindowSize hook returns width and height, but internally it handles mounting, updating on resize, and unmounting without memory leaks. The benefit is not code reuse — it's contract enforcement. Class components required mixins or higher-order components, which broke encapsulation. Custom hooks keep the lifecycle logic local to where state is consumed, not inherited. They also prevent stale closure bugs: hooks re-run on every render, capturing fresh props and state. Use them for any cross-cutting concern that touches DOM, subscriptions, or timers.

CustomHookExample.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// io.thecodeforge — javascript tutorial

import { useState, useEffect } from 'react';

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  // Single contract: mount -> listen, unmount -> remove
  useEffect(() => {
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []); // Empty deps: only mount/unmount

  return isOnline;
}

// Usage: const isOnline = useOnlineStatus();
// No lifecycle boilerplate in consuming component
Output
// Returns true/false based on navigator.onLine
// Updates automatically on network state change
// No cleanup leak — effect returns teardown
Production Trap:
Custom hooks must include cleanup. Missing removeEventListener or clearInterval inside a custom hook leaks across every component that uses it.
Key Takeaway
Custom hooks enforce the lifecycle contract once — every component that uses one gets correct setup and teardown automatically.

Constructor: The Gatekeeper of Component State

Class constructors run before mounting and are invoked with props. They serve as the single location to initialize local state, bind event handlers, and validate props for type consistency. Constructors are not for side effects—no API calls, no subscriptions, no mutations. The fundamental rule: call super(props) first to inherit React.Component behavior. This ensures this.props is accessible throughout the component. State initialization should be static and derived only from props using simple assignments. Any work beyond state initialization and handler binding signals a design issue—extract it to lifecycle methods or custom hooks. Constructors run once per component instance, making them ideal for setting up initial state values that don't depend on async operations.

ConstructorExample.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// io.thecodeforge — javascript tutorial
import React, { Component } from 'react';

class Counter extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: props.initialCount || 0
    };
    this.handleIncrement = this.handleIncrement.bind(this);
  }

  handleIncrement() {
    this.setState(prev => ({ count: prev.count + 1 }));
  }

  render() {
    return <button onClick={this.handleIncrement}>{this.state.count}</button>;
  }
}
Production Trap:
Never put async code, setInterval, or fetch calls inside constructor. Those introduce timing bugs and break React's lifecycle guarantees.
Key Takeaway
Limit constructor to three concerns: super() call, simple state initialization, and handler binding.

Constructor Pitfalls: 20 Years of Bug Reports Condensed

The constructor's most dangerous trap is unintended side effects. Setting state conditionally based on props that change later will fail—the constructor only runs once. Common mistakes: calling this.setState (use direct assignment instead), performing DOM queries, starting timers, or initializing non-React state objects that mutate. Best practices demand static properties for class fields to avoid boilerplate binding. Use class property syntax (handleClick = () => {}) to eliminate constructor handler binding entirely. For props-to-state mapping, use getDerivedStateFromProps only when necessary—direct prop usage in render is cleaner. Constructor calls to external services create tight coupling that breaks testing and SSR. The modern alternative is functional components with useState, which eliminate these entire categories of defects.

ConstructorBestPractices.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// io.thecodeforge — javascript tutorial
class SaferComponent extends Component {
  state = {
    theme: this.props.defaultTheme || 'light'
  };

  handleClick = () => {
    this.setState({ theme: 'dark' });
  };

  render() {
    return <div onClick={this.handleClick}>{this.state.theme}</div>;
  }
}
Key Takeaway:
Class properties eliminate constructor entirely. Your default state lives at the top, handlers are auto-bound, and the constructor death trap disappears.
Key Takeaway
Replace constructor with class property syntax to reduce boilerplate and remove binding errors.
● Production incidentPOST-MORTEMseverity: high

The 3 AM Pager: Memory Leak That Took Down a Trading Dashboard

Symptom
Dashboard tab consumes ~200MB immediately, climbs to 1.5GB+ within 30 minutes. Browser tab crashes or Chrome kills the tab with 'Aw, Snap!' Users on low-end machines report even faster crashes.
Assumption
The team assumed React would automatically clean up timers because the component was managing state. They thought that since the interval called setState, and setState manages component lifecycle, React must handle the cleanup.
Root cause
A setInterval was started in componentDidMount for polling stock prices every 5 seconds. Nobody added clearInterval in componentWillUnmount. When users navigated to other pages in the SPA, the component unmounted but the interval kept firing. Each tick called setState on an unmounted component, and React queued the state updates. Over time, the queued updates accumulated in memory. The browser's event loop kept the interval alive, preventing garbage collection of the component tree. Memory grew linearly with time the dashboard was open.
Fix
Store the interval ID as a class property (this.pollingTimer) in componentDidMount and call clearInterval(this.pollingTimer) inside componentWillUnmount. The hooks equivalent: store the interval in a ref and clear it in the useEffect cleanup function.
Key lesson
  • Every resource you create outside React's managed state must be torn down explicitly.
  • componentWillUnmount is not optional — it's a contract. If you set it up in componentDidMount, tear it down there.
  • The 'Can't perform a React state update on an unmounted component' warning is never a false positive. It always signals a real leak.
  • In functional components, the useEffect cleanup function is called before re-run AND on unmount — use it for both scenario logic.
Production debug guideQuick symptom-to-action mapping for the three most common lifecycle failures in production4 entries
Symptom · 01
API calls fire repeatedly in an infinite loop — browser tab slows to a crawl
Fix
Check componentDidUpdate for missing comparison guard. Look for setState calls that trigger re-renders which re-enter componentDidUpdate. Add if (prevProps.id !== this.props.id) guard around the fetch logic.
Symptom · 02
Console warning: 'Can't perform a React state update on an unmounted component'
Fix
Track down every componentDidMount that creates a timer, subscription, or async fetch. Verify that componentWillUnmount cancels each one. For async fetches, use AbortController. For timers, store the ID and clear it.
Symptom · 03
Data displayed is stale — shows previous query results even after props changed
Fix
Verify that componentDidUpdate watches the relevant props. You likely have data fetching only in componentDidMount but no re-fetch logic when props change. Add comparison guard and re-fetch inside componentDidUpdate.
Symptom · 04
Third-party chart library doesn't render or throws 'container not found'
Fix
Move the chart initialisation from constructor or render to componentDidMount. The DOM node doesn't exist in constructor, and render doesn't guarantee the ref is populated yet. componentDidMount guarantees the node is in the real DOM.
★ Lifecycle Debug Cheat Sheet — Quick Commands for Common ScenariosReach for these patterns when you suspect a lifecycle issue in your React components — each scenario includes the exact fix to apply now.
Infinite re-render after setState in componentDidUpdate
Immediate action
Open DevTools → Components tab → check render count on the component. If it's incrementing every frame, you have a lifecycle loop.
Commands
Add a conditional guard: if (prevProps.userId !== this.props.userId) { this.fetchData(this.props.userId); }
If using hooks, verify the dependency array is correct: useEffect(() => { fetchData(userId); }, [userId]) — never omit deps for side effects.
Fix now
Temporarily comment out the setState call inside componentDidUpdate to verify the loop stops. Then add the comparison guard and uncomment.
setState on unmounted component warning+
Immediate action
Run performance.memory check in browser console to see if heap is growing. A growing heap confirms the leak.
Commands
Add console log inside componentWillUnmount to verify it fires: componentWillUnmount() { console.log('Unmounting', this.constructor.name); clearInterval(this.timer); }
For async cancels: const controller = new AbortController(); fetch(url, { signal: controller.signal }); controller.abort() in willUnmount.
Fix now
Set a component-level mounted flag: this._isMounted = true in componentDidMount, false in componentWillUnmount. Check it before any setState call in async callbacks.
componentDidUpdate not firing when props change+
Immediate action
Verify the parent is actually passing different props — add console.log('Props:', this.props) inside render to see current values.
Commands
Check if shouldComponentUpdate is defined and returning false. Add console.log('SCU:', nextProps, this.props) to debug.
Ensure you're not mutating state directly — setState({ ...state, key: newValue }) creates a new reference. Direct mutation ({ state.key = newValue }) won't trigger re-render or didUpdate.
Fix now
Add a __DEV__ only: componentDidUpdate(prevProps) { Object.keys(this.props).forEach(key => { if (this.props[key] !== prevProps[key]) console.log(key, 'changed'); }); }
AspectClass Lifecycle MethodsuseEffect Hook (Functional)
SyntaxSeparate named methods (componentDidMount, etc.)Single useEffect() with config options
Mount oncecomponentDidMount()useEffect(() => {}, [])
Update on changecomponentDidUpdate(prevProps, prevState)useEffect(() => {}, [dependency])
Cleanup / unmountcomponentWillUnmount()return () => {} inside useEffect
Logic co-locationSetup and teardown split across methodsSetup and teardown in same block
Multiple concernsAll concerns mixed in one methodSeparate useEffect per concern
Learning curveExplicit and readable for beginnersMore composable, but requires mental model shift
Legacy codebase likelihoodVery common in pre-2019 codeStandard in modern React codebases
shouldComponentUpdate equivalentshouldComponentUpdate() methodReact.memo() + useMemo()
Preventing infinite loopsManual comparison guard requiredCorrect dependency array prevents most loops
Error boundary integrationcomponentDidCatch() for error handlingNo direct equivalent — use ErrorBoundary component

Key takeaways

1
componentDidMount fires once after the DOM is ready
it's your safe zone for data fetching, subscriptions, and third-party library setup, not the constructor.
2
componentDidUpdate gives you previousProps and previous State
always compare old vs new before acting, or you'll create an infinite render loop that's painful to debug.
3
componentWillUnmount is non-negotiable cleanup
every setInterval, addEventListener, or WebSocket you create must be destroyed here, or you'll leak memory into every other component in the app.
4
useEffect collapses all three lifecycle phases into one API
the body is mount/update logic, the dependency array controls when it re-runs, and the return function is your cleanup — understanding this mapping makes you fluent in both class and functional React.
5
The event listener reference trap is subtle
anonymous functions in addEventListener can never be removed with removeEventListener — always use a named reference.
6
getDerivedStateFromProps resets state when props change
use it for forms that must clear on new data. getSnapshotBeforeUpdate captures DOM values before an update — use it for scroll position preservation.
7
Error boundaries prevent one component's render error from crashing the whole page. Use getDerivedStateFromError for fallback UI and componentDidCatch for logging. Error boundaries are class components only.

Common mistakes to avoid

5 patterns
×

Missing comparison guard in componentDidUpdate

Symptom
Infinite loop causes browser tab to freeze and console floods with thousands of log lines. The call stack shows repeated calls to componentDidUpdate → setState → render → componentDidUpdate.
Fix
Always wrap your logic with if (previousProps.someValue !== this.props.someValue) before calling setState or firing any side effect inside componentDidUpdate. Never assume componentDidUpdate fires only for the change you care about.
×

Not cleaning up timers or subscriptions in componentWillUnmount

Symptom
React logs 'Warning: Can't perform a React state update on an unmounted component' in the console. Memory grows over time as you navigate through the app, and ghost data appears in other components because stale callbacks update state unexpectedly.
Fix
Store any interval, timeout, or subscription ID in a class property (this.timerID = setInterval(...)) and call clearInterval(this.timerID) inside componentWillUnmount. For event listeners, store the handler as a class property so you can pass the exact same reference to removeEventListener.
×

Fetching data inside render() instead of componentDidMount

Symptom
The API gets called on every single re-render (potentially hundreds of times), you see duplicate network requests in DevTools, and the component flickers constantly because each fetch triggers a state update which triggers another render.
Fix
Move all side effects to componentDidMount or componentDidUpdate. render() must be a pure function with zero side effects — it only describes what the UI looks like given current state and props. If you need to fetch on mount, use componentDidMount. If you need to re-fetch on prop change, use componentDidUpdate with a comparison guard.
×

Using an anonymous arrow function in addEventListener and trying to remove it with removeEventListener

Symptom
The event listener accumulates on every mount — each time the component re-mounts, another listener is added. The old ones are never removed because they reference different function instances.
Fix
Always store the event handler as a class property (handleResize = () => { ... }) or a named function. Use the same reference in both addEventListener('resize', this.handleResize) and removeEventListener('resize', this.handleResize).
×

Not using error boundaries, then wondering why a single component error crashes the whole page

Symptom
A malformed image URL or missing data property causes an entire product page to show a white screen or blank area. The rest of the page (cart, header, footer) could have kept working.
Fix
Wrap major UI sections in error boundaries. Place one around the product grid, one around the cart summary, one around user profile. When a component crashes, the error boundary shows a fallback UI in that section only — the rest of the page continues to function.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Can you walk me through the React component lifecycle phases in order, a...
Q02SENIOR
What happens if you call setState inside componentDidUpdate without a co...
Q03SENIOR
How does useEffect with a dependency array replicate componentDidUpdate ...
Q04SENIOR
What are error boundaries in React? When would you use componentDidCatch...
Q05SENIOR
How would you preserve scroll position when adding new messages to a cha...
Q01 of 05JUNIOR

Can you walk me through the React component lifecycle phases in order, and tell me which method you'd use to fetch data from an API and why?

ANSWER
The lifecycle order is: constructor → render → componentDidMount → (updates) → render → componentDidUpdate → componentWillUnmount. I'd use componentDidMount for the initial API fetch. The constructor runs before the DOM exists, so any setState there is ignored. render runs before the DOM is mounted, so you can't safely interact with the DOM yet. componentDidMount runs after the component is painted to the screen — the DOM node exists, refs are populated, and any setState call will correctly trigger a re-render. For subsequent fetches when props change, I'd use componentDidUpdate with a comparison guard: if (prevProps.userId !== this.props.userId) { this.fetchData(this.props.userId); } In a functional component, I'd use useEffect with an empty dependency array for the initial fetch and useEffect with [userId] for the update scenario.
FAQ · 7 QUESTIONS

Frequently Asked Questions

01
What is the order of React lifecycle methods when a component first loads?
02
Is componentDidMount called on every re-render?
03
Why does React give a warning about setState on an unmounted component, and how do I fix it?
04
What's the difference between useEffect with no deps, empty deps, and populated deps?
05
Can I use async/await in componentDidMount?
06
What is getSnapshotBeforeUpdate used for?
07
How do I create an error boundary in React?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's React.js. Mark it forged?

17 min read · try the examples if you haven't

Previous
React Custom Hooks
15 / 47 · React.js
Next
React Error Boundaries