Episode 2 — React Frontend Architecture NextJS / 2.4 — React Lifecycle Methods
2.4.b — React's Lifecycle Methods
In one sentence: React class components expose a rich set of lifecycle methods —
componentDidMount,shouldComponentUpdate,componentDidUpdate,componentWillUnmount, and more — that give you precise control over what happens at every stage of a component's existence.
Navigation: ← Class Components Lifecycle · Next → useEffect Hook
Table of Contents
- The Complete Lifecycle Map
- Mounting Phase — Birth of a Component
- constructor(props)
- static getDerivedStateFromProps(props, state)
- render()
- componentDidMount()
- Updating Phase — Component Reacts to Change
- shouldComponentUpdate(nextProps, nextState)
- getSnapshotBeforeUpdate(prevProps, prevState)
- componentDidUpdate(prevProps, prevState, snapshot)
- Unmounting Phase — Cleanup
- componentWillUnmount()
- Error Handling Phase
- Deprecated Lifecycle Methods
- PureComponent — Automatic shouldComponentUpdate
- Real-World Class Component — Putting It All Together
- Key Takeaways
1. The Complete Lifecycle Map
┌─────────────────────────────────────────────────────────────────────────────┐
│ REACT CLASS COMPONENT LIFECYCLE │
│ │
│ ╔═══════════════════════════════════════════════════════════════════╗ │
│ ║ MOUNTING ║ │
│ ║ ║ │
│ ║ constructor(props) ║ │
│ ║ ↓ ║ │
│ ║ static getDerivedStateFromProps(props, state) ║ │
│ ║ ↓ ║ │
│ ║ render() ║ │
│ ║ ↓ ║ │
│ ║ ── React updates DOM ── ║ │
│ ║ ↓ ║ │
│ ║ componentDidMount() ← SAFE for side effects ║ │
│ ╚═══════════════════════════════════════════════════════════════════╝ │
│ ↓ │
│ ╔═══════════════════════════════════════════════════════════════════╗ │
│ ║ UPDATING ║ │
│ ║ (triggered by: new props, setState, forceUpdate) ║ │
│ ║ ║ │
│ ║ static getDerivedStateFromProps(props, state) ║ │
│ ║ ↓ ║ │
│ ║ shouldComponentUpdate(nextProps, nextState) ║ │
│ ║ ↓ (if true) ║ │
│ ║ render() ║ │
│ ║ ↓ ║ │
│ ║ getSnapshotBeforeUpdate(prevProps, prevState) ║ │
│ ║ ↓ ║ │
│ ║ ── React updates DOM ── ║ │
│ ║ ↓ ║ │
│ ║ componentDidUpdate(prevProps, prevState, snapshot) ║ │
│ ╚═══════════════════════════════════════════════════════════════════╝ │
│ ↓ │
│ ╔═══════════════════════════════════════════════════════════════════╗ │
│ ║ UNMOUNTING ║ │
│ ║ ║ │
│ ║ componentWillUnmount() ← Cleanup here ║ │
│ ╚═══════════════════════════════════════════════════════════════════╝ │
│ │
│ ╔═══════════════════════════════════════════════════════════════════╗ │
│ ║ ERROR HANDLING (special) ║ │
│ ║ ║ │
│ ║ static getDerivedStateFromError(error) ║ │
│ ║ componentDidCatch(error, info) ║ │
│ ╚═══════════════════════════════════════════════════════════════════╝ │
└─────────────────────────────────────────────────────────────────────────────┘
Quick reference table
| Method | Phase | Can setState? | Can do side effects? | Static? |
|---|---|---|---|---|
constructor | Mount | Assign only | ❌ | No |
getDerivedStateFromProps | Mount + Update | Return new state | ❌ | Yes |
render | Mount + Update | ❌ | ❌ | No |
componentDidMount | Mount | ✅ | ✅ | No |
shouldComponentUpdate | Update | ❌ | ❌ | No |
getSnapshotBeforeUpdate | Update | ❌ | Read DOM | No |
componentDidUpdate | Update | ✅ (guarded) | ✅ | No |
componentWillUnmount | Unmount | ❌ | Cleanup only | No |
getDerivedStateFromError | Error | Return new state | ❌ | Yes |
componentDidCatch | Error | ✅ | ✅ (logging) | No |
2. Mounting Phase — Birth of a Component
When React encounters <MyComponent /> for the first time, it creates an instance and runs:
1. constructor(props) → Initialize state, bind methods
2. getDerivedStateFromProps() → Sync state with props (rare)
3. render() → Return JSX (pure, no side effects)
4. React updates the DOM → Actual DOM nodes created
5. componentDidMount() → Side effects: fetch, subscribe, measure
Example: Full mounting flow
class MountDemo extends Component {
constructor(props) {
super(props);
this.state = { data: null };
console.log('1. constructor');
}
static getDerivedStateFromProps(props, state) {
console.log('2. getDerivedStateFromProps');
return null; // No state change
}
render() {
console.log('3. render');
return <div>{this.state.data || 'Loading...'}</div>;
}
componentDidMount() {
console.log('4. componentDidMount — DOM is ready');
// Safe to fetch, measure, subscribe
}
}
// Console output on mount:
// 1. constructor
// 2. getDerivedStateFromProps
// 3. render
// 4. componentDidMount — DOM is ready
3. constructor(props)
constructor(props) {
super(props); // MUST be first line
// ✅ Initialize state
this.state = {
count: 0,
items: [],
user: null,
};
// ✅ Bind event handlers (if not using arrow fields)
this.handleClick = this.handleClick.bind(this);
// ✅ Create refs
this.inputRef = React.createRef();
}
Do and don't
| ✅ Do | ❌ Don't |
|---|---|
Set initial this.state | Call this.setState() |
| Bind methods | Fetch data |
| Create refs | Subscribe to events |
| Copy props to state (usually) | |
| Cause any side effects |
When you DON'T need a constructor
If you use class fields, you often don't need a constructor at all:
class Modern extends Component {
state = { count: 0 }; // Class field replaces constructor
inputRef = React.createRef(); // Class field replaces constructor
handleClick = () => { }; // Arrow field replaces bind
// No constructor needed!
}
4. static getDerivedStateFromProps(props, state)
This is a static method — it doesn't have access to this. It runs before every render (mount AND update).
static getDerivedStateFromProps(props, state) {
// Return an object to update state, or null for no change
if (props.currentRow !== state.lastRow) {
return {
isScrollingDown: props.currentRow > state.lastRow,
lastRow: props.currentRow,
};
}
return null;
}
When to use it
Almost never. The React docs list it as rarely needed. Common alternatives:
| Problem | Better Solution |
|---|---|
| Reset state when prop changes | Use a key on the component |
| Compute something from props | Compute in render() or use useMemo |
| Fetch data when prop changes | Use componentDidUpdate |
The key trick instead of getDerivedStateFromProps
// ❌ Complex: Track prop changes and reset state
class ProfileEditor extends Component {
static getDerivedStateFromProps(props, state) {
if (props.userId !== state.prevUserId) {
return { prevUserId: props.userId, name: '', email: '' };
}
return null;
}
}
// ✅ Simple: Change the key to remount with fresh state
<ProfileEditor key={userId} userId={userId} />
When the key changes, React unmounts the old instance and mounts a fresh one — no manual state reset needed.
5. render()
The only required method. Must be pure — no side effects.
render() {
const { title, items } = this.props;
const { loading, error } = this.state;
if (error) return <ErrorMessage error={error} />;
if (loading) return <Spinner />;
return (
<section>
<h1>{title}</h1>
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</section>
);
}
Rules
- Must return JSX, array, fragment, string, number, boolean, null, or portal
- Must be pure — same props + state = same output
- No setState — causes infinite loop
- No DOM access — DOM may not exist yet (during mount)
- Called frequently — must be fast
render is called more than you think
Parent re-renders → Child render() called
this.setState() → render() called
this.forceUpdate() → render() called (skips shouldComponentUpdate)
Context value changes → render() called
This is why shouldComponentUpdate and React.memo exist — to skip unnecessary renders.
6. componentDidMount()
Called once, immediately after the component is inserted into the DOM. This is the #1 most important lifecycle method.
componentDidMount() {
// The DOM is ready. The component is visible. Go wild.
}
Common use cases
1. Fetch data
componentDidMount() {
this.fetchData();
}
fetchData = async () => {
try {
const res = await fetch(`/api/posts?userId=${this.props.userId}`);
const posts = await res.json();
this.setState({ posts, loading: false });
} catch (error) {
this.setState({ error: error.message, loading: false });
}
};
2. Subscribe to events
componentDidMount() {
window.addEventListener('resize', this.handleResize);
window.addEventListener('scroll', this.handleScroll);
this.unsubscribe = store.subscribe(this.handleStoreChange);
}
// Don't forget cleanup in componentWillUnmount!
3. Set up timers
componentDidMount() {
this.intervalId = setInterval(() => {
this.setState(prev => ({ seconds: prev.seconds + 1 }));
}, 1000);
}
4. Measure DOM elements
componentDidMount() {
const rect = this.containerRef.current.getBoundingClientRect();
this.setState({
width: rect.width,
height: rect.height,
});
}
5. Integrate third-party libraries
componentDidMount() {
this.chart = new Chart(this.canvasRef.current, {
type: 'line',
data: this.props.data,
options: this.props.options,
});
}
Can I call setState in componentDidMount?
Yes. It will trigger a second render, but the user won't see the intermediate state (React batches the paint). Use it for:
- Setting state based on DOM measurements
- Setting state after fetching data
componentDidMount() {
// This causes two renders, but the first is never visible to the user
const height = this.ref.current.scrollHeight;
this.setState({ scrollHeight: height });
}
7. Updating Phase — Component Reacts to Change
Three things trigger an update:
- New props from parent
setState()callforceUpdate()call
The update flow:
getDerivedStateFromProps(props, state) ← Rarely needed
↓
shouldComponentUpdate(nextProps, nextState) ← Performance gate
↓ (if returns true — default)
render() ← Generate new Virtual DOM
↓
getSnapshotBeforeUpdate(prevProps, prevState) ← Read DOM before update
↓
── React updates the real DOM ──
↓
componentDidUpdate(prevProps, prevState, snapshot) ← Side effects
8. shouldComponentUpdate(nextProps, nextState)
A performance optimization method. Return false to skip re-rendering.
shouldComponentUpdate(nextProps, nextState) {
// Only re-render if count actually changed
return nextProps.count !== this.props.count;
}
Default behaviour
If not defined, returns true — component always re-renders when parent does.
When to use it
class ExpensiveList extends Component {
shouldComponentUpdate(nextProps) {
// Only re-render if the items array reference changed
return nextProps.items !== this.props.items;
}
render() {
return (
<ul>
{this.props.items.map(item => (
<ExpensiveRow key={item.id} data={item} />
))}
</ul>
);
}
}
Gotchas
shouldComponentUpdate(nextProps) {
// ❌ WRONG — shallow comparison misses deep changes
return nextProps.config !== this.props.config;
// If parent creates new object each render: { theme: 'dark' }
// This will ALWAYS return true (different reference)
// ❌ WRONG — deep comparison is expensive
return JSON.stringify(nextProps) !== JSON.stringify(this.props);
// Defeats the purpose of optimization
}
What forceUpdate() does
this.forceUpdate();
// Skips shouldComponentUpdate entirely
// Goes straight to render()
// Use VERY sparingly — usually means your state model is wrong
9. getSnapshotBeforeUpdate(prevProps, prevState)
Called after render() but before the DOM is updated. Lets you capture information from the DOM that's about to change.
getSnapshotBeforeUpdate(prevProps, prevState) {
// Capture scroll position before new items shift content
if (prevState.items.length < this.state.items.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop; // Distance from bottom
}
return null;
}
Whatever you return becomes the third argument of componentDidUpdate.
Classic use case: Preserving scroll position
class ChatLog extends Component {
listRef = React.createRef();
getSnapshotBeforeUpdate(prevProps) {
// If new messages were added, capture scroll info
if (prevProps.messages.length < this.props.messages.length) {
const list = this.listRef.current;
return {
scrollHeight: list.scrollHeight,
scrollTop: list.scrollTop,
};
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (snapshot !== null) {
const list = this.listRef.current;
// Maintain scroll position relative to bottom
list.scrollTop = list.scrollHeight - snapshot.scrollHeight + snapshot.scrollTop;
}
}
render() {
return (
<div ref={this.listRef} style={{ overflow: 'auto', maxHeight: 400 }}>
{this.props.messages.map(msg => (
<div key={msg.id}>{msg.text}</div>
))}
</div>
);
}
}
When to use
| Use Case | What to Capture |
|---|---|
| Chat scroll position | scrollHeight, scrollTop |
| Animation start values | Element dimensions, positions |
| Focus management | Currently focused element |
This method is rarely needed. Most apps never use it.
10. componentDidUpdate(prevProps, prevState, snapshot)
Called after every update (not on initial mount). The DOM has been updated.
componentDidUpdate(prevProps, prevState, snapshot) {
// prevProps = props BEFORE the update
// prevState = state BEFORE the update
// snapshot = return value of getSnapshotBeforeUpdate
}
Common patterns
1. Fetch data when props change
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.fetchUserData(this.props.userId);
}
}
CRITICAL: Always guard with a condition!
// ❌ INFINITE LOOP — setState triggers update, update calls setState
componentDidUpdate() {
this.setState({ updated: true });
}
// ✅ Guarded — only runs when condition changes
componentDidUpdate(prevProps) {
if (prevProps.id !== this.props.id) {
this.setState({ data: null, loading: true });
this.fetchData(this.props.id);
}
}
2. Update third-party library
componentDidUpdate(prevProps) {
if (prevProps.data !== this.props.data) {
this.chart.data = this.props.data;
this.chart.update();
}
if (prevProps.options !== this.props.options) {
this.chart.options = this.props.options;
this.chart.update();
}
}
3. Respond to state changes
componentDidUpdate(prevProps, prevState) {
if (prevState.query !== this.state.query) {
this.debouncedSearch(this.state.query);
}
if (!prevState.isOpen && this.state.isOpen) {
// Modal just opened — focus the first input
this.inputRef.current?.focus();
}
}
4. Use snapshot data
componentDidUpdate(prevProps, prevState, snapshot) {
if (snapshot?.shouldScrollToBottom) {
this.messagesEnd.current.scrollIntoView({ behavior: 'smooth' });
}
}
Can I call setState?
Yes, but it must be wrapped in a condition to prevent infinite loops:
componentDidUpdate(prevProps) {
// ✅ Conditional setState
if (prevProps.locale !== this.props.locale) {
this.setState({ translations: loadTranslations(this.props.locale) });
}
}
11. Unmounting Phase — Cleanup
When a component is removed from the DOM (parent stops rendering it, route changes, conditional render turns false), React calls one method:
componentWillUnmount()
↓
React removes DOM nodes
↓
Instance is garbage collected
12. componentWillUnmount()
Called once, right before the component is destroyed. This is where you clean up.
componentWillUnmount() {
// Clean up everything you set up in componentDidMount
}
What to clean up
1. Timers
componentDidMount() {
this.intervalId = setInterval(() => this.tick(), 1000);
}
componentWillUnmount() {
clearInterval(this.intervalId);
}
2. Event listeners
componentDidMount() {
window.addEventListener('resize', this.handleResize);
document.addEventListener('keydown', this.handleKeyDown);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
document.removeEventListener('keydown', this.handleKeyDown);
}
3. Network requests
componentDidMount() {
this.controller = new AbortController();
fetch('/api/data', { signal: this.controller.signal })
.then(res => res.json())
.then(data => this.setState({ data }))
.catch(err => {
if (err.name !== 'AbortError') this.setState({ error: err.message });
});
}
componentWillUnmount() {
this.controller.abort();
}
4. Subscriptions
componentDidMount() {
this.unsubscribe = eventBus.on('notification', this.handleNotification);
}
componentWillUnmount() {
this.unsubscribe();
}
5. Third-party libraries
componentDidMount() {
this.chart = new Chart(this.canvasRef.current, config);
}
componentWillUnmount() {
this.chart.destroy(); // Release canvas, event listeners, animation frames
}
What NOT to do in componentWillUnmount
componentWillUnmount() {
// ❌ setState is pointless — component is dying
this.setState({ farewell: true });
// ❌ Will cause "setState on unmounted component" warning
fetch('/api/logout').then(() => this.setState({ loggedOut: true }));
}
Memory leak checklist
┌──────────────────────────────────────────────────────────────┐
│ MEMORY LEAK PREVENTION CHECKLIST │
│ │
│ Did you set up... Then clean up in willUnmount: │
│ ───────────────── ────────────────────────── │
│ setInterval / setTimeout clearInterval / clearTimeout │
│ addEventListener removeEventListener │
│ WebSocket connection ws.close() │
│ fetch / XMLHttpRequest AbortController.abort() │
│ store.subscribe() unsubscribe() │
│ observer.observe() observer.disconnect() │
│ chart / map library instance.destroy() │
│ MutationObserver observer.disconnect() │
│ IntersectionObserver observer.disconnect() │
│ ResizeObserver observer.disconnect() │
│ MediaQueryList.addListener removeListener │
└──────────────────────────────────────────────────────────────┘
13. Error Handling Phase
React provides two methods for catching errors in child components:
static getDerivedStateFromError(error)
Called when a child component throws during rendering. Use it to show a fallback UI.
class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
// Update state to show fallback UI
return { hasError: true, errorMessage: error.message };
}
render() {
if (this.state.hasError) {
return <h2>Something went wrong: {this.state.errorMessage}</h2>;
}
return this.props.children;
}
}
componentDidCatch(error, info)
Called after an error is thrown. Use it for logging — it has access to the component stack trace.
class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, info) {
// info.componentStack contains the component tree stack trace
console.error('Error:', error);
console.error('Component stack:', info.componentStack);
// Send to error tracking service
Sentry.captureException(error, {
extra: { componentStack: info.componentStack }
});
}
render() {
if (this.state.hasError) {
return this.props.fallback || <h2>Something went wrong</h2>;
}
return this.props.children;
}
}
What errors do they catch?
| Catches | Doesn't Catch |
|---|---|
Errors in render() | Event handlers |
| Errors in lifecycle methods | Async code (setTimeout, fetch) |
| Errors in constructors of children | Server-side rendering |
Errors in static getDerivedStateFromProps | Errors in the boundary itself |
Error boundary usage patterns
// 1. Wrap the entire app
<ErrorBoundary>
<App />
</ErrorBoundary>
// 2. Wrap individual features (RECOMMENDED)
<Layout>
<ErrorBoundary fallback={<p>Sidebar failed</p>}>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary fallback={<p>Content failed</p>}>
<MainContent />
</ErrorBoundary>
</Layout>
// 3. Wrap routes
<Route path="/dashboard">
<ErrorBoundary fallback={<DashboardError />}>
<Dashboard />
</ErrorBoundary>
</Route>
Complete production error boundary
class ErrorBoundary extends Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, info) {
logErrorToService(error, info.componentStack);
}
handleReset = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
// Custom fallback
if (this.props.fallback) {
return typeof this.props.fallback === 'function'
? this.props.fallback({ error: this.state.error, reset: this.handleReset })
: this.props.fallback;
}
// Default fallback
return (
<div role="alert" className="error-boundary">
<h2>Something went wrong</h2>
<pre>{this.state.error?.message}</pre>
<button onClick={this.handleReset}>Try again</button>
</div>
);
}
return this.props.children;
}
}
// Usage with render-prop fallback
<ErrorBoundary
fallback={({ error, reset }) => (
<div>
<p>Error: {error.message}</p>
<button onClick={reset}>Retry</button>
</div>
)}
>
<FeatureComponent />
</ErrorBoundary>
14. Deprecated Lifecycle Methods
React 16.3 deprecated three methods and React 17+ shows warnings when using them:
componentWillMount() (UNSAFE)
// ❌ DEPRECATED — renamed to UNSAFE_componentWillMount
UNSAFE_componentWillMount() {
// Called before render on mount
// People misused this for data fetching — but the fetch wouldn't
// complete before render anyway, making it pointless
}
Why removed: It ran before render, giving a false sense that data would be ready. It also causes problems with server-side rendering (called on server too) and async rendering.
Migration: Move logic to constructor or componentDidMount.
componentWillReceiveProps(nextProps) (UNSAFE)
// ❌ DEPRECATED
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.userId !== this.props.userId) {
this.setState({ user: null });
this.fetchUser(nextProps.userId);
}
}
Why removed: People used it to "sync state with props" which led to bugs. It fires even when props haven't actually changed (parent re-rendered with same props).
Migration: Use componentDidUpdate or getDerivedStateFromProps.
componentWillUpdate(nextProps, nextState) (UNSAFE)
// ❌ DEPRECATED
UNSAFE_componentWillUpdate(nextProps, nextState) {
if (nextProps.items.length !== this.props.items.length) {
this.scrollPosition = this.listRef.scrollTop;
}
}
Why removed: Reading DOM in this method was unreliable with async rendering because the DOM might change between this call and the actual update.
Migration: Use getSnapshotBeforeUpdate.
Migration summary
| Deprecated | Replace With |
|---|---|
componentWillMount | constructor or componentDidMount |
componentWillReceiveProps | componentDidUpdate or getDerivedStateFromProps |
componentWillUpdate | getSnapshotBeforeUpdate |
15. PureComponent — Automatic shouldComponentUpdate
PureComponent performs a shallow comparison of props and state automatically:
import { PureComponent } from 'react';
class ExpensiveRow extends PureComponent {
// No shouldComponentUpdate needed!
// React automatically does shallow comparison of all props and state
render() {
return (
<tr>
<td>{this.props.name}</td>
<td>{this.props.email}</td>
<td>{this.props.role}</td>
</tr>
);
}
}
How shallow comparison works
// Shallow compare checks:
// For each key in nextProps and nextState:
// Object.is(current[key], next[key])
// PRIMITIVES — works great
{ count: 5 } vs { count: 5 } // Same — skip render ✅
{ count: 5 } vs { count: 6 } // Different — re-render
// OBJECTS — compares by reference only
{ user: { name: 'A' } }
vs
{ user: { name: 'A' } } // Different objects — re-render! (even though values match)
// ARRAYS — same issue
{ items: [1, 2, 3] }
vs
{ items: [1, 2, 3] } // Different arrays — re-render!
When PureComponent helps
// ✅ Good — parent passes primitive props
<ExpensiveRow name="Priya" email="priya@example.com" role="admin" />
// ✅ Good — parent memoizes object/array props
class Parent extends Component {
// Only creates new array when this.state.items changes
getFilteredItems = () => this.state.items.filter(i => i.active);
render() {
return <ExpensiveList items={this.state.cachedItems} />;
}
}
When PureComponent doesn't help
// ❌ Bad — new object every render defeats PureComponent
class Parent extends Component {
render() {
return (
<PureChild
style={{ color: 'red' }} // New object each render!
onClick={() => this.handle()} // New function each render!
data={this.state.items.filter( // New array each render!
i => i.active
)}
/>
);
}
}
PureComponent vs Component vs React.memo
Component | PureComponent | React.memo | |
|---|---|---|---|
| Type | Class | Class | Function wrapper |
| Auto shallow compare | ❌ | ✅ | ✅ (props only) |
| Compares state | N/A | ✅ | N/A |
| Custom comparison | shouldComponentUpdate | Can override | Second argument |
| Use with | Class components | Class components | Function components |
16. Real-World Class Component — Putting It All Together
Let's build a complete, production-quality class component that uses every major lifecycle method:
import React, { Component, createRef } from 'react';
/**
* StockTicker — Real-time stock price display with WebSocket
*
* Uses: constructor, componentDidMount, shouldComponentUpdate,
* getSnapshotBeforeUpdate, componentDidUpdate,
* componentWillUnmount, render
*/
class StockTicker extends Component {
// ─── State & Refs ─────────────────────────────
state = {
prices: [],
connected: false,
error: null,
autoScroll: true,
};
listRef = createRef();
ws = null;
reconnectTimer = null;
reconnectAttempts = 0;
maxReconnectAttempts = 5;
// ─── Mounting ─────────────────────────────────
componentDidMount() {
console.log('[StockTicker] Mounted — connecting to WebSocket');
this.connect();
}
// ─── Updating ─────────────────────────────────
shouldComponentUpdate(nextProps, nextState) {
// Skip re-render if only reconnectAttempts changed (not in state)
// Always re-render if prices, connected, or error changed
if (
nextState.prices === this.state.prices &&
nextState.connected === this.state.connected &&
nextState.error === this.state.error &&
nextProps.symbol === this.props.symbol
) {
return false;
}
return true;
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// If new prices were added, capture scroll position
if (prevState.prices.length < this.state.prices.length && this.listRef.current) {
const list = this.listRef.current;
const isAtBottom = list.scrollHeight - list.scrollTop <= list.clientHeight + 50;
return { isAtBottom };
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// Reconnect if symbol changed
if (prevProps.symbol !== this.props.symbol) {
console.log(`[StockTicker] Symbol changed: ${prevProps.symbol} → ${this.props.symbol}`);
this.disconnect();
this.setState({ prices: [], error: null });
this.connect();
}
// Auto-scroll to bottom if user was already at bottom
if (snapshot?.isAtBottom && this.state.autoScroll && this.listRef.current) {
this.listRef.current.scrollTop = this.listRef.current.scrollHeight;
}
}
// ─── Unmounting ───────────────────────────────
componentWillUnmount() {
console.log('[StockTicker] Unmounting — cleaning up');
this.disconnect();
clearTimeout(this.reconnectTimer);
}
// ─── Methods ──────────────────────────────────
connect = () => {
try {
this.ws = new WebSocket(`wss://api.example.com/stocks/${this.props.symbol}`);
this.ws.onopen = () => {
console.log('[StockTicker] Connected');
this.reconnectAttempts = 0;
this.setState({ connected: true, error: null });
};
this.ws.onmessage = (event) => {
const price = JSON.parse(event.data);
this.setState(prev => ({
prices: [...prev.prices.slice(-99), price], // Keep last 100
}));
};
this.ws.onclose = () => {
this.setState({ connected: false });
this.attemptReconnect();
};
this.ws.onerror = (error) => {
this.setState({ error: 'Connection error' });
};
} catch (err) {
this.setState({ error: err.message });
}
};
disconnect = () => {
if (this.ws) {
this.ws.onclose = null; // Prevent reconnect on intentional close
this.ws.close();
this.ws = null;
}
};
attemptReconnect = () => {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
this.setState({ error: 'Max reconnection attempts reached' });
return;
}
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
this.reconnectAttempts++;
this.reconnectTimer = setTimeout(() => {
console.log(`[StockTicker] Reconnecting (attempt ${this.reconnectAttempts})`);
this.connect();
}, delay);
};
formatPrice = (price) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(price);
};
// ─── Render ───────────────────────────────────
render() {
const { symbol } = this.props;
const { prices, connected, error } = this.state;
return (
<div className="stock-ticker">
<header>
<h2>{symbol}</h2>
<span className={`status ${connected ? 'online' : 'offline'}`}>
{connected ? '● Connected' : '○ Disconnected'}
</span>
</header>
{error && <div className="error">{error}</div>}
<div ref={this.listRef} className="price-list">
{prices.length === 0 ? (
<p className="empty">Waiting for data...</p>
) : (
prices.map((p, i) => (
<div
key={`${p.timestamp}-${i}`}
className={`price-row ${p.change > 0 ? 'up' : 'down'}`}
>
<span>{this.formatPrice(p.price)}</span>
<span>{p.change > 0 ? '▲' : '▼'} {Math.abs(p.change).toFixed(2)}%</span>
<time>{new Date(p.timestamp).toLocaleTimeString()}</time>
</div>
))
)}
</div>
{prices.length > 0 && (
<footer>
<span>Latest: {this.formatPrice(prices[prices.length - 1].price)}</span>
<span>{prices.length} updates</span>
</footer>
)}
</div>
);
}
}
export default StockTicker;
Lifecycle methods used in this example
| Method | Purpose in StockTicker |
|---|---|
componentDidMount | Connect WebSocket |
shouldComponentUpdate | Skip render when no visible changes |
getSnapshotBeforeUpdate | Capture scroll position before new prices |
componentDidUpdate | Reconnect on symbol change, auto-scroll |
componentWillUnmount | Close WebSocket, clear timers |
17. Key Takeaways
-
Mounting:
constructor→getDerivedStateFromProps→render→ DOM update →componentDidMount. Fetch data and subscribe incomponentDidMount. -
Updating: Triggered by new props,
setState, orforceUpdate. UseshouldComponentUpdateto optimize. Always guardsetStateincomponentDidUpdatewith a condition to prevent infinite loops. -
Unmounting:
componentWillUnmountis your only chance to clean up timers, listeners, subscriptions, and connections. Memory leaks happen when you skip this. -
Error Handling:
getDerivedStateFromError+componentDidCatch= Error Boundaries. Still requires class components (no hook equivalent). -
Deprecated methods:
componentWillMount,componentWillReceiveProps,componentWillUpdateare gone. Useconstructor/componentDidMount,componentDidUpdate/getDerivedStateFromProps, andgetSnapshotBeforeUpdaterespectively. -
PureComponent does shallow comparison automatically — great for primitive props, unreliable for objects/arrays created each render.
-
render()must be pure — no side effects, nosetState. It's called on every update. -
getDerivedStateFromPropsis almost never the right solution — prefer thekeytrick orcomponentDidUpdate. -
getSnapshotBeforeUpdatecaptures pre-update DOM state — niche but irreplaceable for scroll preservation. -
Every
componentDidMountsetup should have a correspondingcomponentWillUnmountcleanup.
Explain-It Challenge
-
The Restaurant Kitchen: Think of a React component as a restaurant.
componentDidMountis when the restaurant opens for the day (set up equipment, preheat ovens).componentDidUpdateis when a new order comes in (only cook what changed, don't remake the whole menu).componentWillUnmountis closing time (turn off ovens, clean up, lock doors). What happens if a restaurant opens (mounts) but never closes (unmounts cleanup)? How does this map to memory leaks? -
The Bouncer:
shouldComponentUpdateis like a bouncer at a club. Every time someone (a re-render) tries to get in, the bouncer checks: "Are you actually different from the person already inside?" If not, they're turned away (render skipped). Why isPureComponentlike a bouncer with a checklist who only looks at surface features (shallow compare), and why might identical twins (objects with same values but different references) fool this bouncer? -
The Fire Drill:
getDerivedStateFromError+componentDidCatchare like a fire alarm and fire escape. The alarm (getDerivedStateFromError) triggers immediately — switch to evacuation mode (fallback UI). The fire escape (componentDidCatch) is for logging — call the fire department (error tracking service). Why can't event handler errors trigger this alarm? (Hint: they happen outside the rendering phase.)
Navigation: ← Class Components Lifecycle · Next → useEffect Hook