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

  1. The Complete Lifecycle Map
  2. Mounting Phase — Birth of a Component
  3. constructor(props)
  4. static getDerivedStateFromProps(props, state)
  5. render()
  6. componentDidMount()
  7. Updating Phase — Component Reacts to Change
  8. shouldComponentUpdate(nextProps, nextState)
  9. getSnapshotBeforeUpdate(prevProps, prevState)
  10. componentDidUpdate(prevProps, prevState, snapshot)
  11. Unmounting Phase — Cleanup
  12. componentWillUnmount()
  13. Error Handling Phase
  14. Deprecated Lifecycle Methods
  15. PureComponent — Automatic shouldComponentUpdate
  16. Real-World Class Component — Putting It All Together
  17. 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

MethodPhaseCan setState?Can do side effects?Static?
constructorMountAssign onlyNo
getDerivedStateFromPropsMount + UpdateReturn new stateYes
renderMount + UpdateNo
componentDidMountMountNo
shouldComponentUpdateUpdateNo
getSnapshotBeforeUpdateUpdateRead DOMNo
componentDidUpdateUpdate✅ (guarded)No
componentWillUnmountUnmountCleanup onlyNo
getDerivedStateFromErrorErrorReturn new stateYes
componentDidCatchError✅ (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.stateCall this.setState()
Bind methodsFetch data
Create refsSubscribe 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:

ProblemBetter Solution
Reset state when prop changesUse a key on the component
Compute something from propsCompute in render() or use useMemo
Fetch data when prop changesUse 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

  1. Must return JSX, array, fragment, string, number, boolean, null, or portal
  2. Must be pure — same props + state = same output
  3. No setState — causes infinite loop
  4. No DOM access — DOM may not exist yet (during mount)
  5. 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:

  1. New props from parent
  2. setState() call
  3. forceUpdate() 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 CaseWhat to Capture
Chat scroll positionscrollHeight, scrollTop
Animation start valuesElement dimensions, positions
Focus managementCurrently 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?

CatchesDoesn't Catch
Errors in render()Event handlers
Errors in lifecycle methodsAsync code (setTimeout, fetch)
Errors in constructors of childrenServer-side rendering
Errors in static getDerivedStateFromPropsErrors 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

DeprecatedReplace With
componentWillMountconstructor or componentDidMount
componentWillReceivePropscomponentDidUpdate or getDerivedStateFromProps
componentWillUpdategetSnapshotBeforeUpdate

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

ComponentPureComponentReact.memo
TypeClassClassFunction wrapper
Auto shallow compare✅ (props only)
Compares stateN/AN/A
Custom comparisonshouldComponentUpdateCan overrideSecond argument
Use withClass componentsClass componentsFunction 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

MethodPurpose in StockTicker
componentDidMountConnect WebSocket
shouldComponentUpdateSkip render when no visible changes
getSnapshotBeforeUpdateCapture scroll position before new prices
componentDidUpdateReconnect on symbol change, auto-scroll
componentWillUnmountClose WebSocket, clear timers

17. Key Takeaways

  1. Mounting: constructorgetDerivedStateFromPropsrender → DOM update → componentDidMount. Fetch data and subscribe in componentDidMount.

  2. Updating: Triggered by new props, setState, or forceUpdate. Use shouldComponentUpdate to optimize. Always guard setState in componentDidUpdate with a condition to prevent infinite loops.

  3. Unmounting: componentWillUnmount is your only chance to clean up timers, listeners, subscriptions, and connections. Memory leaks happen when you skip this.

  4. Error Handling: getDerivedStateFromError + componentDidCatch = Error Boundaries. Still requires class components (no hook equivalent).

  5. Deprecated methods: componentWillMount, componentWillReceiveProps, componentWillUpdate are gone. Use constructor/componentDidMount, componentDidUpdate/getDerivedStateFromProps, and getSnapshotBeforeUpdate respectively.

  6. PureComponent does shallow comparison automatically — great for primitive props, unreliable for objects/arrays created each render.

  7. render() must be pure — no side effects, no setState. It's called on every update.

  8. getDerivedStateFromProps is almost never the right solution — prefer the key trick or componentDidUpdate.

  9. getSnapshotBeforeUpdate captures pre-update DOM state — niche but irreplaceable for scroll preservation.

  10. Every componentDidMount setup should have a corresponding componentWillUnmount cleanup.


Explain-It Challenge

  1. The Restaurant Kitchen: Think of a React component as a restaurant. componentDidMount is when the restaurant opens for the day (set up equipment, preheat ovens). componentDidUpdate is when a new order comes in (only cook what changed, don't remake the whole menu). componentWillUnmount is 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?

  2. The Bouncer: shouldComponentUpdate is 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 is PureComponent like 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?

  3. The Fire Drill: getDerivedStateFromError + componentDidCatch are 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