This synthesizes every phase covered in Modules 2 and 3 into one cohesive component: a live analytics dashboard that streams metrics over a WebSocket, tracks window size for responsive chart re-layout, preserves scroll position when new rows arrive via a snapshot, skips wasted re-renders with shouldComponentUpdate, and is wrapped in its own isolated Error Boundary so a rendering bug in the dashboard cannot take down the rest of the app.
src/components/AnalyticsDashboard.jsxjavascript
import { Component, createRef } from 'react'
import ErrorBoundary from './ErrorBoundary.jsx'
class AnalyticsDashboardInner extends Component {
feedListRef = createRef()
socket = null
resizeObserver = null
isUnmounted = false
state = {
events: [],
viewportWidth: window.innerWidth,
connectionStatus: 'connecting'
}
componentDidMount() {
this.connectSocket()
// ResizeObserver on the panel itself (not just window) — catches layout
// changes from sidebar toggles, not only browser window resizing.
this.resizeObserver = new ResizeObserver((entries) => {
const width = entries[0]?.contentRect.width
if (width) this.setState({ viewportWidth: width })
})
if (this.feedListRef.current) {
this.resizeObserver.observe(this.feedListRef.current)
}
}
connectSocket() {
this.socket = new WebSocket('wss://api.example.com/analytics/stream')
this.socket.addEventListener('open', () => {
if (!this.isUnmounted) this.setState({ connectionStatus: 'connected' })
})
this.socket.addEventListener('message', (event) => {
if (this.isUnmounted) return
const incomingEvent = JSON.parse(event.data)
this.setState((prev) => ({
// Cap the buffer so a long-running dashboard session doesn't
// accumulate unbounded memory from an endless event stream.
events: [...prev.events, incomingEvent].slice(-500)
}))
})
this.socket.addEventListener('close', () => {
if (!this.isUnmounted) this.setState({ connectionStatus: 'disconnected' })
})
}
// Performance guardrail: only re-render when the event count changes or
// the viewport crosses a breakpoint that actually affects layout — not on
// every pixel of a drag-resize.
shouldComponentUpdate(nextProps, nextState) {
const eventsChanged = nextState.events.length !== this.state.events.length
const statusChanged = nextState.connectionStatus !== this.state.connectionStatus
const prevBreakpoint = this.state.viewportWidth < 768 ? 'narrow' : 'wide'
const nextBreakpoint = nextState.viewportWidth < 768 ? 'narrow' : 'wide'
const breakpointChanged = prevBreakpoint !== nextBreakpoint
return eventsChanged || statusChanged || breakpointChanged
}
// Pin scroll position when new events arrive, exactly like the chat
// example in Module 2 — read the pre-commit scroll state here.
getSnapshotBeforeUpdate(prevProps, prevState) {
if (prevState.events.length === this.state.events.length) return null
const list = this.feedListRef.current
if (!list) return null
const distanceFromBottom = list.scrollHeight - list.scrollTop - list.clientHeight
return distanceFromBottom < 80 ? 'pin-to-bottom' : null
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (snapshot === 'pin-to-bottom' && this.feedListRef.current) {
const list = this.feedListRef.current
list.scrollTop = list.scrollHeight
}
}
componentWillUnmount() {
this.isUnmounted = true
this.resizeObserver?.disconnect()
this.socket?.close()
}
render() {
const { events, connectionStatus, viewportWidth } = this.state
const isNarrow = viewportWidth < 768
return (
<section className={`analytics-dashboard ${isNarrow ? 'analytics-dashboard--narrow' : ''}`}>
<header className="analytics-dashboard__header">
<h2>Live Analytics</h2>
<span className={`status-pill status-pill--${connectionStatus}`}>
{connectionStatus}
</span>
</header>
<ul className="analytics-dashboard__feed" ref={this.feedListRef}>
{events.map((evt) => (
<li key={evt.id} className="analytics-dashboard__event">
<span className="analytics-dashboard__event-type">{evt.type}</span>
<span className="analytics-dashboard__event-value">{evt.value}</span>
</li>
))}
</ul>
</section>
)
}
}
// The dashboard is deliberately wrapped in its own ErrorBoundary at export
// time — a malformed event payload or a chart-rendering bug inside this
// subtree shows a local fallback instead of unmounting the entire app shell.
function AnalyticsDashboard(props) {
return (
<ErrorBoundary>
<AnalyticsDashboardInner {...props} />
</ErrorBoundary>
)
}
export default AnalyticsDashboardsrc/components/AnalyticsDashboard.csscss
.analytics-dashboard {
display: flex;
flex-direction: column;
height: 480px;
border: 1px solid #2a2f3a;
border-radius: 12px;
background: #12151c;
color: #e6e8eb;
overflow: hidden;
}
.analytics-dashboard__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.25rem;
border-bottom: 1px solid #2a2f3a;
}
.analytics-dashboard__header h2 {
margin: 0;
font-size: 1.05rem;
font-weight: 600;
}
.status-pill {
padding: 0.25rem 0.65rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.status-pill--connected {
background: rgba(34, 197, 94, 0.15);
color: #4ade80;
}
.status-pill--connecting {
background: rgba(234, 179, 8, 0.15);
color: #facc15;
}
.status-pill--disconnected {
background: rgba(239, 68, 68, 0.15);
color: #f87171;
}
.analytics-dashboard__feed {
flex: 1;
margin: 0;
padding: 0.5rem 0;
list-style: none;
overflow-y: auto;
}
.analytics-dashboard__event {
display: flex;
justify-content: space-between;
padding: 0.6rem 1.25rem;
border-bottom: 1px solid #1c1f27;
font-size: 0.875rem;
}
.analytics-dashboard__event-type {
color: #93c5fd;
font-weight: 500;
}
.analytics-dashboard--narrow .analytics-dashboard__event {
flex-direction: column;
gap: 0.15rem;
}
.error-boundary-fallback {
padding: 1.5rem;
border: 1px solid #f87171;
border-radius: 12px;
background: rgba(239, 68, 68, 0.08);
color: #fca5a5;
}| Hook | Order | Phase | setState safe? | Primary use case |
|---|---|---|---|---|
| constructor(props) | 1 (mount only) | Render | Direct assignment only (this.state = {...}), never this.setState() | Initialize state, bind handlers |
| static getDerivedStateFromProps(props, state) | 2 (mount), 1 (update) | Render | N/A — returns state object or null, no imperative setState | Rare: reset controlled state when a prop-driven record id changes |
| render() | 3 (mount), 3 (update) | Render | No | Return JSX — pure function of props/state |
| shouldComponentUpdate(nextProps, nextState) | 2 (update only) | Render | No | Skip render()/commit for performance |
| getSnapshotBeforeUpdate(prevProps, prevState) | 4 (update only) | Render (pre-commit) | No | Read DOM state before mutations apply (e.g. scroll position) |
| componentDidMount() | 4 (mount only) | Commit | Yes | Data fetching, subscriptions, DOM-dependent library init |
| componentDidUpdate(prevProps, prevState, snapshot) | 5 (update only) | Commit | Yes (must guard with a prop/state comparison) | Sync with external systems, conditional re-fetching |
| componentWillUnmount() | 1 (unmount only) | Commit | No — instance is being discarded | Clear timers, close sockets, remove listeners, disconnect observers |
| static getDerivedStateFromError(error) | 1 (on descendant throw) | Render | N/A — returns state object | Compute fallback UI state |
| componentDidCatch(error, info) | 2 (on descendant throw) | Commit | Yes | Log to monitoring/telemetry service |
Capstone Takeaways
- A production-grade class component composes every phase together: mount-time setup, update-time guardrails and snapshots, and unmount-time teardown all coexist in the same instance.
- Wrapping a complex subtree in a dedicated Error Boundary at the export boundary isolates failures — a bug in the dashboard's render doesn't unmount the rest of the application shell.
- shouldComponentUpdate and getSnapshotBeforeUpdate are what separate a merely correct class component from a performant one under real, high-frequency data (WebSocket ticks, resize events).
- Every subscription opened in componentDidMount (socket, observer, listener) has a mirrored teardown call in componentWillUnmount — treat this pairing as non-negotiable in code review.