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.
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 LiveMonitorPanelAn 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.
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 ErrorBoundaryHigher-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.
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 withAuthimport { 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.
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 MouseTrackerimport MouseTracker from './MouseTracker.jsx'
function CursorSpotlight() {
return (
<MouseTracker>
{({ x, y }) => (
<div
className="cursor-spotlight"
style={{ transform: `translate(${x}px, ${y}px)` }}
/>
)}
</MouseTracker>
)
}
export default CursorSpotlightContext 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).
import { createContext } from 'react'
const ThemeContext = createContext({ mode: 'light', toggle: () => {} })
export default ThemeContextimport { 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 ThemedPanelimport { 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 MultiContextPanelModule 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.