componentWillUnmount()

Purpose: garbage collection. Trigger: exactly once, immediately before React removes the component's DOM node and destroys the instance. Anything the component created that outlives a normal garbage-collection pass — timers, subscriptions, observers, listeners on window/document — must be explicitly torn down here, or it leaks: the callback keeps a closure reference to the unmounted instance, so neither the instance nor whatever it's holding can be collected.

src/components/LiveMonitorPanel.jsxjavascript
import { Component, createRef } from 'react'

class LiveMonitorPanel extends Component {
  intersectionRef = createRef()
  socket = null
  observer = null
  pollTimerId = null

  state = { isVisible: false, messages: [], serverTimeSkewMs: 0 }

  componentDidMount() {
    // 1. IntersectionObserver — pauses/resumes rendering-heavy work based on
    // viewport visibility. Left uncleaned, the browser keeps invoking the
    // callback against a DOM node that's no longer in the tree.
    this.observer = new IntersectionObserver(
      ([entry]) => this.setState({ isVisible: entry.isIntersecting }),
      { threshold: 0.1 }
    )
    this.observer.observe(this.intersectionRef.current)

    // 2. WebSocket — an open socket with attached listeners is a classic
    // leak source. The connection itself keeps running server-side and the
    // event handlers keep firing into a dead component instance.
    this.socket = new WebSocket('wss://api.example.com/monitor')
    this.socket.addEventListener('message', this.handleSocketMessage)

    // 3. Custom global event listener.
    window.addEventListener('keydown', this.handleGlobalKeydown)

    // 4. Active timer — setInterval keeps firing forever unless cleared,
    // each tick holding a live reference to `this`.
    this.pollTimerId = setInterval(this.pollServerTime, 30000)
  }

  handleSocketMessage = (event) => {
    const message = JSON.parse(event.data)
    this.setState((prev) => ({ messages: [...prev.messages, message] }))
  }

  handleGlobalKeydown = (event) => {
    if (event.key === 'Escape') this.props.onClose?.()
  }

  pollServerTime = async () => {
    const response = await fetch('/api/time')
    const { serverTimeMs } = await response.json()
    this.setState({ serverTimeSkewMs: serverTimeMs - Date.now() })
  }

  componentWillUnmount() {
    // Teardown must mirror setup exactly, item for item.
    this.observer?.disconnect()

    this.socket?.removeEventListener('message', this.handleSocketMessage)
    this.socket?.close()

    window.removeEventListener('keydown', this.handleGlobalKeydown)

    clearInterval(this.pollTimerId)
  }

  render() {
    const { isVisible, messages } = this.state
    return (
      <div className="live-monitor-panel" ref={this.intersectionRef}>
        <p>Visible: {isVisible ? 'yes' : 'no'}</p>
        <ul>
          {messages.map((message) => (
            <li key={message.id}>{message.text}</li>
          ))}
        </ul>
      </div>
    )
  }
}

export default LiveMonitorPanel

An Error Boundary is a class component implementing static getDerivedStateFromError and/or componentDidCatch. It catches JavaScript errors thrown anywhere in the render phase of its child tree — not in event handlers, not in async code, not in the boundary's own render — and swaps in a fallback UI instead of letting the error crash the entire app with an unmounted white screen.

static getDerivedStateFromError(error)

Purpose: run during the render phase, immediately after a descendant throws, to compute the state that renders the fallback UI. It is static and must be pure — no logging or side effects here, since (like other render-phase methods) it can be invoked more than once per actual commit under React's internals.

componentDidCatch(error, info)

Purpose: run during the commit phase, after the fallback UI has already been rendered, specifically to perform side effects — logging the error to a monitoring service. info.componentStack gives the component tree at the point of the crash, which is invaluable for tracing which nested feature broke.

src/components/ErrorBoundary.jsxjavascript
import { Component } from 'react'

class ErrorBoundary extends Component {
  state = { hasError: false, error: null }

  static getDerivedStateFromError(error) {
    // Pure state transition only — swaps the boundary into fallback mode.
    return { hasError: true, error }
  }

  componentDidCatch(error, info) {
    // Side effects belong here, not in getDerivedStateFromError.
    // Replace with your actual telemetry client (e.g. Sentry.captureException).
    reportToMonitoring({
      error: error.message,
      stack: error.stack,
      componentStack: info.componentStack
    })
  }

  handleReset = () => {
    this.setState({ hasError: false, error: null })
  }

  render() {
    if (this.state.hasError) {
      return (
        <div role="alert" className="error-boundary-fallback">
          <h2>Something went wrong.</h2>
          <p>{this.state.error?.message}</p>
          <button type="button" onClick={this.handleReset}>
            Try again
          </button>
        </div>
      )
    }

    return this.props.children
  }
}

function reportToMonitoring(payload) {
  // Stand-in for a real integration, e.g.:
  // Sentry.captureException(new Error(payload.error), { extra: payload })
  fetch('/api/telemetry/errors', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload)
  }).catch(() => {
    // Never let telemetry failures cascade into another render-time throw.
  })
}

export default ErrorBoundary

Higher-Order Components (HOCs)

A HOC is a function that takes a component and returns a new component wrapping it with injected behavior — the class-era equivalent of a custom Hook. Below, withAuth injects an authenticated user object and blocks rendering the wrapped component until an auth check resolves.

src/hocs/withAuth.jsxjavascript
import { Component } from 'react'

/**
 * withAuth(WrappedComponent)
 * Injects `currentUser` and `authError` props into WrappedComponent after
 * verifying the session server-side. Renders nothing meaningful until the
 * check resolves, to avoid flashing protected content before auth confirms.
 */
function withAuth(WrappedComponent) {
  class WithAuth extends Component {
    state = { currentUser: null, authError: null, isChecking: true }

    async componentDidMount() {
      try {
        const response = await fetch('/api/session', { credentials: 'include' })
        if (!response.ok) throw new Error('Not authenticated')
        const currentUser = await response.json()
        if (!this.isUnmounted) {
          this.setState({ currentUser, isChecking: false })
        }
      } catch (error) {
        if (!this.isUnmounted) {
          this.setState({ authError: error.message, isChecking: false })
        }
      }
    }

    componentWillUnmount() {
      this.isUnmounted = true
    }

    render() {
      const { isChecking, currentUser, authError } = this.state

      if (isChecking) return <p>Verifying session…</p>
      if (authError) return <p role="alert">Authentication failed: {authError}</p>

      // Spread the original props through, plus the injected auth data —
      // WrappedComponent never needs to know how the user was fetched.
      return <WrappedComponent {...this.props} currentUser={currentUser} />
    }
  }

  // Preserve a readable name in React DevTools instead of "WithAuth".
  WithAuth.displayName = `withAuth(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`

  return WithAuth
}

export default withAuth
src/components/AdminPanel.jsx (usage)javascript
import { Component } from 'react'
import withAuth from '../hocs/withAuth.jsx'

class AdminPanel extends Component {
  render() {
    // currentUser is injected by withAuth — AdminPanel itself has no
    // knowledge of the session-check mechanism.
    return <h1>Welcome, {this.props.currentUser.name}</h1>
  }
}

export default withAuth(AdminPanel)

The Render Props Pattern

Instead of wrapping a component, a render-props component takes a function as children (or a named prop) and calls it with internal state, letting the consumer decide what to render. This avoids prop-name collisions that can occur when composing multiple HOCs.

src/components/MouseTracker.jsxjavascript
import { Component } from 'react'

/**
 * MouseTracker
 * Tracks pointer position and passes { x, y } to a render-prop function.
 * Uses shouldComponentUpdate to skip re-renders when the cursor hasn't
 * moved by a meaningful amount — pointermove fires at high frequency and
 * re-rendering on every 1px jitter is wasted work.
 */
class MouseTracker extends Component {
  state = { x: 0, y: 0 }

  componentDidMount() {
    window.addEventListener('pointermove', this.handlePointerMove)
  }

  componentWillUnmount() {
    window.removeEventListener('pointermove', this.handlePointerMove)
  }

  handlePointerMove = (event) => {
    this.setState({ x: event.clientX, y: event.clientY })
  }

  shouldComponentUpdate(nextProps, nextState) {
    const dx = Math.abs(nextState.x - this.state.x)
    const dy = Math.abs(nextState.y - this.state.y)
    return dx > 2 || dy > 2
  }

  render() {
    // this.props.children is a function: (position) => ReactNode
    return this.props.children(this.state)
  }
}

export default MouseTracker
src/components/CursorSpotlight.jsx (usage)javascript
import MouseTracker from './MouseTracker.jsx'

function CursorSpotlight() {
  return (
    <MouseTracker>
      {({ x, y }) => (
        <div
          className="cursor-spotlight"
          style={{ transform: `translate(${x}px, ${y}px)` }}
        />
      )}
    </MouseTracker>
  )
}

export default CursorSpotlight

Context API Integration

Class components cannot call useContext, but they have two mechanisms: static contextType for consuming exactly one context via this.context, and <Context.Consumer> for consuming one or more contexts via render props (composable when a component needs multiple contexts at once).

src/context/ThemeContext.jsjavascript
import { createContext } from 'react'

const ThemeContext = createContext({ mode: 'light', toggle: () => {} })

export default ThemeContext
src/components/ThemedPanel.jsx (single context via static contextType)javascript
import { Component } from 'react'
import ThemeContext from '../context/ThemeContext.js'

class ThemedPanel extends Component {
  // static contextType wires this.context to ThemeContext's current value.
  // Limitation: a class can only subscribe to ONE context this way.
  static contextType = ThemeContext

  render() {
    const { mode, toggle } = this.context
    return (
      <div className={`panel panel--${mode}`}>
        <button type="button" onClick={toggle}>
          Switch to {mode === 'light' ? 'dark' : 'light'} mode
        </button>
      </div>
    )
  }
}

export default ThemedPanel
src/components/MultiContextPanel.jsx (multiple contexts via Consumer)javascript
import { Component } from 'react'
import ThemeContext from '../context/ThemeContext.js'
import LocaleContext from '../context/LocaleContext.js'

class MultiContextPanel extends Component {
  render() {
    // Nested Consumers let a class read multiple contexts — static
    // contextType can only bind one, so this is the fallback for classes
    // that need both theme and locale simultaneously.
    return (
      <ThemeContext.Consumer>
        {({ mode }) => (
          <LocaleContext.Consumer>
            {({ locale }) => (
              <div className={`panel panel--${mode}`}>
                <p>Locale: {locale}</p>
              </div>
            )}
          </LocaleContext.Consumer>
        )}
      </ThemeContext.Consumer>
    )
  }
}

export default MultiContextPanel

Module 3 Takeaways

  • componentWillUnmount must tear down every subscription created in componentDidMount, one-to-one — observers, sockets, listeners, and timers.
  • getDerivedStateFromError computes fallback state (pure, render phase); componentDidCatch performs logging side effects (commit phase).
  • Error Boundaries only catch render-phase errors in descendants — not event handlers, async callbacks, or their own render.
  • HOCs wrap a component to inject behavior; Render Props pass a function as children to hand control of rendering to the consumer — both remain valid class-era composition patterns.
  • static contextType binds exactly one context to this.context; nested <Context.Consumer> elements are required to read multiple contexts in a class component.