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.
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
importReact, { Component } from'react';
// A simple component that logs each lifecycle phase clearly// so you can see the ORDER in which they fire in the consoleclassUserProfileCardextendsComponent {
constructor(props) {
super(props);
// constructor runs first — before anything is painted to the DOMthis.state = {
profileData: null,
isLoading: true,
};
console.log('[1] constructor — component instance created');
}
// Fires ONCE after the component appears in the DOMcomponentDidMount() {
console.log('[3] componentDidMount — component is now visible in the DOM');
// Safe to fetch data here because the DOM node existsthis.fetchUserProfile(this.props.userId);
}
// Fires after EVERY re-render caused by props or state changecomponentDidUpdate(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 DOMcomponentWillUnmount() {
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 leaksclearTimeout(this.refreshTimer);
}
fetchUserProfile(userId) {
this.setState({ isLoading: true });
// Simulating an API call with setTimeoutthis.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>
);
}
}
exportdefaultUserProfileCard;
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
importReact, { Component } from'react';
// Real-world pattern: a dashboard that fetches data on mount// AND sets up a polling interval to refresh every 60 secondsclassWeatherDashboardextendsComponent {
constructor(props) {
super(props);
this.state = {
weatherData: null,
errorMessage: null,
lastUpdated: null,
};
// We'll store the interval ID so we can clear it on unmountthis.pollingInterval = null;
}
asynccomponentDidMount() {
// First fetch happens immediately when the component mountsawaitthis.loadWeatherData();
// Then we set up polling — fetch fresh data every 60 seconds// Store the interval ID so componentWillUnmount can clean it upthis.pollingInterval = setInterval(async () => {
console.log('Polling: refreshing weather data...');
awaitthis.loadWeatherData();
}, 60000); // 60,000ms = 60 seconds
}
asyncloadWeatherData() {
const { cityName } = this.props;
try {
// In a real app this would be: fetch(`/api/weather?city=${cityName}`)const response = awaitfetch(
`https://api.open-meteo.com/v1/forecast?latitude=51.5&longitude=-0.1¤t_weather=true`
);
if (!response.ok) {
thrownewError(`API returned status ${response.status}`);
}
const data = await response.json();
// setState here is safe — component is definitely mountedthis.setState({
weatherData: data.current_weather,
errorMessage: null,
lastUpdated: newDate().toLocaleTimeString(),
});
} catch (fetchError) {
// Always handle errors — never let a rejected promise go silentthis.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 componentif (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>LondonWeather</h3>
<p>Temperature: {weatherData.temperature}°C</p>
<p>WindSpeed: {weatherData.windspeed} km/h</p>
<small>Last updated: {lastUpdated}</small>
</div>
);
}
}
exportdefaultWeatherDashboard;
Output
// On mount:
Polling: refreshing weather data... (fires after 60s)
Polling: refreshing weather data... (fires after 120s)
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
importReact, { Component } from'react';
// Real-world pattern: a report that re-fetches when filters change// This is the most common use case for componentDidUpdateclassSalesReportTableextendsComponent {
constructor(props) {
super(props);
this.state = {
reportRows: [],
isFetching: false,
totalRevenue: 0,
};
}
componentDidMount() {
// Fetch the initial report using the starting prop valuesthis.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 fetchesconst 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 loopif (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 addedif (previousProps.reportRows !== this.state.reportRows && this.tableRef) {
this.tableRef.scrollTop = this.tableRef.scrollHeight;
}
}
asyncfetchSalesReport(startDate, endDate, region) {
// Set loading state — triggers a re-render but componentDidUpdate// won't re-fetch because the props haven't changedthis.setState({ isFetching: true });
try {
// Simulated API responseconst 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>
SalesReport: {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>
);
}
}
exportdefaultSalesReportTable;
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
importReact, { 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 leaksclassChatPanelextendsComponent {
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 WebSocketthis.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 oneif (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 listenerthis.disconnectFromChat();
// Clear any scheduled reconnectif (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 messageconst 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 delaysetTimeout(() => {
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>
);
}
}
exportdefaultChatPanel;
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
importReact, { Component, useState, useEffect } from'react';
// ─────────────────────────────────────────────────────────// CLASS COMPONENT VERSION — explicit lifecycle methods// ─────────────────────────────────────────────────────────classNotificationPanelClassextendsComponent {
constructor(props) {
super(props);
this.state = { notifications: [], connectionStatus: 'disconnected' };
this.socketConnection = null;
}
componentDidMount() {
// Set up a WebSocket-like subscription when the component mountsthis.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 oneif (previousProps.userId !== this.props.userId) {
this.disconnectFromStream();
this.connectToNotificationStream(this.props.userId);
}
}
componentWillUnmount() {
// Always close the connection when the panel is hidden/removedthis.disconnectFromStream();
}
connectToNotificationStream(userId) {
console.log(`Connecting to stream for user: ${userId}`);
this.setState({ connectionStatus: 'connected' });
// Simulating a real-time notification streamthis.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// ─────────────────────────────────────────────────────────functionNotificationPanelHooks({ 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 logicreturn () => {
clearInterval(streamInterval);
setConnectionStatus('disconnected');
console.log('Disconnected from notification stream');
};
}, [userId]); // Only re-run this effect when userId changesreturn (
<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 Method
Phase
Hooks Equivalent
When to Use
constructor
Mounting
useState initialiser
Set initial state and bind methods. Avoid side effects.
static getDerivedStateFromProps
Mount/Update
useState + useEffect with dep, or key prop
Rarely needed. Used when state must be synced with props before render (e.g., form reset on user change).
shouldComponentUpdate
Update
React.memo (for props), useMemo, useCallback
Performance optimisation. Prevent re-render when props/state haven't changed.
render
All
Function body
Pure function that returns JSX. No side effects.
getSnapshotBeforeUpdate
Update
useLayoutEffect
Capture DOM values (scroll position, cursor) before update. Used with componentDidUpdate.
componentDidMount
Mounting
useEffect(() => {}, [])
Data fetching, subscriptions, DOM manipulation after component is visible.
componentDidUpdate
Update
useEffect(() => {}, [dep])
React to prop/state changes with side effects. Always compare prev vs current.
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
importReact, { Component } from'react';
// ============================================================// EXAMPLE 1: getDerivedStateFromProps — Reset form when userId changes// ============================================================classUserProfileFormextendsComponent {
constructor(props) {
super(props);
this.state = {
name: '',
email: '',
prevUserId: props.userId, // Track the last userId that caused a reset
};
}
staticgetDerivedStateFromProps(nextProps, prevState) {
// If the userId prop changed, reset the form fieldsif (nextProps.userId !== prevState.prevUserId) {
return {
name: '',
email: '',
prevUserId: nextProps.userId,
};
}
// No state update neededreturnnull;
}
handleChange = (field, value) => {
this.setState({ [field]: value });
};
render() {
const { name, email } = this.state;
const { userId } = this.props;
return (
<div>
<h3>EditingUser: {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// ============================================================classExpensiveChartextendsComponent {
// Only re-render if the data array reference or color actually changed// Without this, every parent re-render would trigger a full chart re-drawshouldComponentUpdate(nextProps, nextState) {
// Compare data references (assume immutable data)if (this.props.data !== nextProps.data) returntrue;
// Compare color stringif (this.props.color !== nextProps.color) returntrue;
// No changes that affect the chartreturnfalse;
}
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// ============================================================classChatMessageListextendsComponent {
constructor(props) {
super(props);
this.messageContainerRef = React.createRef();
this.state = { messages: [] };
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// Capture the scroll height before the DOM updatesconst container = this.messageContainerRef.current;
if (container) {
return {
scrollHeightBefore: container.scrollHeight,
scrollTopBefore: container.scrollTop,
};
}
returnnull;
}
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 bottomconst wasNearBottom = snapshot.scrollTopBefore + container.clientHeight >=
snapshot.scrollHeightBefore - 20;
if (wasNearBottom) {
container.scrollTop = newScrollHeight;
}
}
}
addMessage = () => {
this.setState((prev) => ({
messages: [
...prev.messages,
`Message at ${newDate().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}>AddMessage</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.
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.
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.
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.
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
importReact, { Component } from'react';
// ============================================================// ERROR BOUNDARY COMPONENT — Catches render errors// ============================================================classErrorBoundaryextendsComponent {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
};
}
// static method 1: Update state to show fallback UIstaticgetDerivedStateFromError(error) {
// Return an object to update statereturn { hasError: true };
}
// instance method 2: Log the error to an external servicecomponentDidCatch(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 friendlyreturn (
<div className="error-fallback">
<h2>Something went wrong</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
<summary>Technicaldetails (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 normallyreturnthis.props.children;
}
}
// ============================================================// COMPONENT THAT THROWS ON PURPOSE (for testing)// ============================================================constBuggyCounter = () => {
const [count, setCount] = React.useState(0);
if (count === 5) {
// This error will be caught by the closest error boundarythrownewError('Intentional crash at count = 5');
}
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
// ============================================================// USING THE ERROR BOUNDARY// ============================================================constApp = () => {
return (
<div>
<h1>MyApp</h1>
{/* Wrap risky component in error boundary */}
<ErrorBoundary>
<BuggyCounter />
</ErrorBoundary>
{/* This component continues working even ifBuggyCounter 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
// 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 tutorialclassUserProfileextendsReact.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 herethis.fetchUser();
}
render() {
return <div>{this.state.loading ? 'Loading...' : this.state.user.name}</div>;
}
}
// With hooks — no constructor at allfunctionUserProfile() {
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.
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.
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 tutorialclassSubscriptionManagerextendsReact.Component {
// Contract: mount -> setup; unmount -> teardowncomponentDidMount() {
this.interval = setInterval(this.syncData, 1000);
}
// Violation: no cleanup = memory leak per mount+unmount cyclecomponentWillUnmount() {
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.
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.
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.
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.
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'); }); }
Aspect
Class Lifecycle Methods
useEffect Hook (Functional)
Syntax
Separate named methods (componentDidMount, etc.)
Single useEffect() with config options
Mount once
componentDidMount()
useEffect(() => {}, [])
Update on change
componentDidUpdate(prevProps, prevState)
useEffect(() => {}, [dependency])
Cleanup / unmount
componentWillUnmount()
return () => {} inside useEffect
Logic co-location
Setup and teardown split across methods
Setup and teardown in same block
Multiple concerns
All concerns mixed in one method
Separate useEffect per concern
Learning curve
Explicit and readable for beginners
More composable, but requires mental model shift
Legacy codebase likelihood
Very common in pre-2019 code
Standard in modern React codebases
shouldComponentUpdate equivalent
shouldComponentUpdate() method
React.memo() + useMemo()
Preventing infinite loops
Manual comparison guard required
Correct dependency array prevents most loops
Error boundary integration
componentDidCatch() for error handling
No 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.
Q02 of 05SENIOR
What happens if you call setState inside componentDidUpdate without a conditional check? How would you debug that in production?
ANSWER
You get an infinite render loop. setState inside componentDidUpdate triggers a re-render, which triggers componentDidUpdate again, which calls setState again — this cycle continues until the browser tab freezes or the call stack overflows.
To debug in production: open the browser DevTools Performance tab and record a profile. The call stack will show repeating calls to componentDidUpdate → setState → render → componentDidUpdate. The render count column in the React DevTools Components tab will increment on every frame.
The fix is immediately obvious: add a conditional comparison guard. But if the app is already frozen, you need to deploy a hotfix that comments out the setState call in componentDidUpdate, then add the guard and redeploy.
In the hooks equivalent, the same bug happens when you call setState inside a useEffect without specifying the correct dependency array, or when the state setter is called inside the effect body without proper conditions.
Q03 of 05SENIOR
How does useEffect with a dependency array replicate componentDidUpdate — and how is it different from calling componentDidUpdate in a class component with the same prop comparison logic?
ANSWER
useEffect with a dependency array like [userId] runs after the component mounts AND after every render where userId has changed. The dependency array is essentially a built-in comparison guard — React compares the current value of each dependency to its previous value and only re-runs the effect if something changed.
In a class component, componentDidUpdate receives prevProps and prevState explicitly, and you write your own comparison logic. This gives you more granular control — you can compare multiple props in one method and decide exactly what to do based on which combination changed.
The key difference: useEffect has closure semantics. The effect function captures the values at the time the effect runs, which can lead to stale closures if you're not careful (the exhaustive-deps ESLint rule catches this). componentDidUpdate always receives the current instance values via this.props and this.state, so there's no stale closure risk.
Another difference: useEffect runs after layout and paint by default (unless you use useLayoutEffect). componentDidUpdate runs synchronously after the DOM updates but before the browser paints — this matters if you need to read DOM measurements before paint.
Q04 of 05SENIOR
What are error boundaries in React? When would you use componentDidCatch vs getDerivedStateFromError?
ANSWER
Error boundaries are components that catch JavaScript errors during rendering, in lifecycle methods, or in constructors of components below them. They prevent the entire component tree from unmounting and showing a white screen — instead, they display a fallback UI.
getDerivedStateFromError is a static method used to update state — typically setting hasError: true — so the render method can show a fallback UI. It's called during the render phase, so it must be pure and have no side effects.
componentDidCatch is an instance method used for side effects — logging the error to an external service like Sentry, or incrementing an error counter. It receives the error object and an info object containing the component stack trace.
Use both together: getDerivedStateFromError updates state to show the fallback UI, componentDidCatch logs the error for debugging. Neither exists in hooks — error boundaries must be class components.
Critical limitation: error boundaries 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.
Q05 of 05SENIOR
How would you preserve scroll position when adding new messages to a chat component? Which lifecycle method would you use and why?
ANSWER
I would use getSnapshotBeforeUpdate paired with componentDidUpdate.
getSnapshotBeforeUpdate fires right before React applies the DOM updates from a render. I would capture the current scroll height and scroll top of the message container, then store them as a snapshot.
componentDidUpdate receives that snapshot as its third argument. After React updates the DOM, I compare the new scroll height to the old one. If the user was scrolled near the bottom (within 20 pixels), I can automatically scroll to the new bottom. This preserves the user's reading position while still showing new messages.
Without getSnapshotBeforeUpdate, any scroll position logic in componentDidUpdate would happen after the DOM has already changed, causing the scroll position to jump.
In functional components, useLayoutEffect can capture the scroll position synchronously, but getSnapshotBeforeUpdate is the class-component pattern designed specifically for this use case.
01
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?
JUNIOR
02
What happens if you call setState inside componentDidUpdate without a conditional check? How would you debug that in production?
SENIOR
03
How does useEffect with a dependency array replicate componentDidUpdate — and how is it different from calling componentDidUpdate in a class component with the same prop comparison logic?
SENIOR
04
What are error boundaries in React? When would you use componentDidCatch vs getDerivedStateFromError?
SENIOR
05
How would you preserve scroll position when adding new messages to a chat component? Which lifecycle method would you use and why?
SENIOR
FAQ · 7 QUESTIONS
Frequently Asked Questions
01
What is the order of React lifecycle methods when a component first loads?
The order is: constructor → render → componentDidMount. The constructor sets up initial state, render describes the UI structure, and componentDidMount fires after the component is actually inserted into the real DOM. This is why data fetching belongs in componentDidMount and not in the constructor — the DOM doesn't exist yet when the constructor runs.
Was this helpful?
02
Is componentDidMount called on every re-render?
No — componentDidMount fires exactly once per component instance, right after the initial mount. Subsequent re-renders (caused by state or prop changes) trigger componentDidUpdate instead. If you want code that runs on every render, you'd use componentDidUpdate, but almost always with a conditional guard to avoid infinite loops.
Was this helpful?
03
Why does React give a warning about setState on an unmounted component, and how do I fix it?
This warning appears when an async operation (like a fetch call) completes after the component has already been removed from the DOM, and then calls setState. The fix is to either cancel the async operation in componentWillUnmount using an AbortController (for fetch requests) or a clearTimeout/clearInterval for timers, or track a mounted flag — set this._isMounted = true in componentDidMount and check it before calling setState in your async callback.
Was this helpful?
04
What's the difference between useEffect with no deps, empty deps, and populated deps?
useEffect with no dependency array at all runs after every single render — including the initial mount. This is almost never what you want and usually indicates a bug. useEffect with an empty array [] runs once after the initial mount — equivalent to componentDidMount. useEffect with a populated array [value] runs after the initial mount AND after every render where value changed — equivalent to componentDidMount + componentDidUpdate for that specific value. The cleanup function (return) runs before the next effect run and on unmount — equivalent to componentWillUnmount.
Was this helpful?
05
Can I use async/await in componentDidMount?
Yes, you can mark componentDidMount as async. But there's a catch: React doesn't await the async function. If the component unmounts before the async operation completes, the callback will try to call setState on an unmounted component. The fix: use an AbortController for fetch requests, or track a mounted flag (this._isMounted) that you set to false in componentWillUnmount. Check it before calling setState in the async callback.
Was this helpful?
06
What is getSnapshotBeforeUpdate used for?
getSnapshotBeforeUpdate is called right before React applies the DOM changes from a render. It captures values from the DOM — like scroll position, cursor location, or element dimensions — and returns them as a snapshot. That snapshot is passed to componentDidUpdate, where you can use it to restore the DOM after the update. The classic use case is preserving scroll position when adding new messages to a chat list: capture scrollHeight in getSnapshotBeforeUpdate, then restore scroll position in componentDidUpdate.
Was this helpful?
07
How do I create an error boundary in React?
Error boundaries are class components only — there is no hook equivalent. Implement either static getDerivedStateFromError(error) to update state and show a fallback UI, componentDidCatch(error, errorInfo) to log the error, or both. Error boundaries catch errors only during rendering, in lifecycle methods, and in constructors — not in event handlers or async code. For those, use try/catch and local error state. Wrap error boundaries around major UI sections so a failure in one area doesn't crash the whole page.