Real-Time Monitoring with React
How to build real-time monitoring dashboards in React with WebSocket data feeds, live charts, auto-refresh patterns, and efficient rendering for high-frequency updates.
Operations dashboards must show the current state of the system, not the state from five minutes ago. During an incident, the difference between real-time and near-real-time visibility is the difference between catching a problem as it develops and discovering it after it has cascaded.
The Web Ops Console uses WebSocket connections to stream real-time metrics, log entries, and alerts to the dashboard. This article covers the patterns for consuming real-time data in React while maintaining smooth performance.
WebSocket Data Provider
A React context provider manages the WebSocket connection lifecycle and distributes incoming messages to subscribed components.
import { createContext, useContext, useEffect, useRef, useState, useCallback } from "react";
interface WSMessage {
channel: string;
data: unknown;
timestamp: string;
}
interface WSContextValue {
isConnected: boolean;
subscribe: (channel: string, handler: (data: unknown) => void) => () => void;
send: (message: unknown) => void;
}
const WSContext = createContext<WSContextValue | null>(null);
function WebSocketProvider({ url, children }: {
url: string;
children: React.ReactNode;
}) {
const wsRef = useRef<WebSocket | null>(null);
const handlersRef = useRef(new Map<string, Set<(data: unknown) => void>>());
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
function connect() {
const ws = new WebSocket(url);
ws.onopen = () => setIsConnected(true);
ws.onclose = () => {
setIsConnected(false);
setTimeout(connect, 3000);
};
ws.onmessage = (event) => {
const message: WSMessage = JSON.parse(event.data);
const handlers = handlersRef.current.get(message.channel);
if (handlers) {
handlers.forEach((handler) => handler(message.data));
}
};
wsRef.current = ws;
}
connect();
return () => wsRef.current?.close();
}, [url]);
const subscribe = useCallback(
(channel: string, handler: (data: unknown) => void) => {
if (!handlersRef.current.has(channel)) {
handlersRef.current.set(channel, new Set());
}
handlersRef.current.get(channel)!.add(handler);
return () => {
handlersRef.current.get(channel)?.delete(handler);
};
},
[]
);
const send = useCallback((message: unknown) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify(message));
}
}, []);
return (
<WSContext.Provider value={{ isConnected, subscribe, send }}>
{children}
</WSContext.Provider>
);
}
function useWebSocket() {
const context = useContext(WSContext);
if (!context) throw new Error("useWebSocket must be used within WSProvider");
return context;
}
function useChannel<T>(channel: string): T | null {
const { subscribe } = useWebSocket();
const [data, setData] = useState<T | null>(null);
useEffect(() => {
return subscribe(channel, (incoming) => setData(incoming as T));
}, [channel, subscribe]);
return data;
}Live Metric Display
Real-time metrics update frequently — CPU usage, request rate, error count. The rendering must be efficient to handle updates every second without janking the UI.
interface LiveMetric {
value: number;
unit: string;
status: "normal" | "warning" | "critical";
}
function LiveMetricCard({ channel, label }: {
channel: string;
label: string;
}) {
const metric = useChannel<LiveMetric>(channel);
const prevValueRef = useRef<number>(0);
const statusColors = {
normal: "text-green-600 bg-green-50",
warning: "text-yellow-600 bg-yellow-50",
critical: "text-red-600 bg-red-50",
};
if (!metric) return <MetricSkeleton />;
const trend = metric.value > prevValueRef.current ? "up" : "down";
prevValueRef.current = metric.value;
return (
<div className={`rounded-lg p-4 ${statusColors[metric.status]}`}>
<div className="text-xs font-medium uppercase tracking-wide opacity-75">
{label}
</div>
<div className="mt-1 flex items-baseline gap-1">
<span className="text-2xl font-bold">
{formatNumber(metric.value)}
</span>
<span className="text-sm opacity-75">{metric.unit}</span>
<TrendArrow direction={trend} />
</div>
</div>
);
}Streaming Charts
Live charts that display time-series data require careful buffer management. Each data point is appended to a rolling window, and old data is pruned to prevent unbounded memory growth.
import { useRef, useEffect, useState, useCallback } from "react";
function useTimeSeriesBuffer(channel: string, maxPoints: number = 300) {
const { subscribe } = useWebSocket();
const bufferRef = useRef<Array<{ time: number; value: number }>>([]);
const [data, setData] = useState<Array<{ time: number; value: number }>>([]);
useEffect(() => {
const unsubscribe = subscribe(channel, (incoming: unknown) => {
const point = incoming as { time: number; value: number };
bufferRef.current.push(point);
// Prune old data
if (bufferRef.current.length > maxPoints) {
bufferRef.current = bufferRef.current.slice(-maxPoints);
}
});
// Batch UI updates at 1fps to avoid excessive re-renders
const updateInterval = setInterval(() => {
setData([...bufferRef.current]);
}, 1000);
return () => {
unsubscribe();
clearInterval(updateInterval);
};
}, [channel, maxPoints, subscribe]);
return data;
}
function LiveChart({ channel, title, color }: {
channel: string;
title: string;
color: string;
}) {
const data = useTimeSeriesBuffer(channel);
return (
<div className="rounded-lg border bg-white p-4">
<h3 className="mb-3 text-sm font-medium text-gray-700">{title}</h3>
<div className="h-48">
<LineChart
data={data}
xKey="time"
yKey="value"
color={color}
animate={false}
xFormatter={(t) => new Date(t).toLocaleTimeString()}
/>
</div>
</div>
);
}The key optimization is decoupling the data arrival rate from the render rate. Data arrives via WebSocket at potentially high frequency (10+ messages per second). But the chart renders at 1 fps by batching updates — smooth enough for visual monitoring without wasting render cycles.
Connection Status Indicator
Operations teams must know if they are seeing live data or stale data. A connection status indicator is essential.
function ConnectionStatus() {
const { isConnected } = useWebSocket();
return (
<div className="flex items-center gap-2 text-sm">
<div
className={`h-2 w-2 rounded-full ${
isConnected ? "bg-green-500 animate-pulse" : "bg-red-500"
}`}
/>
<span className={isConnected ? "text-green-700" : "text-red-700"}>
{isConnected ? "Live" : "Disconnected — reconnecting..."}
</span>
</div>
);
}Service Health Grid
A common ops dashboard view is a grid showing the health status of all services at a glance.
interface ServiceHealth {
name: string;
status: "healthy" | "degraded" | "down";
uptime: number;
latencyP99: number;
errorRate: number;
lastChecked: string;
}
function ServiceHealthGrid() {
const services = useChannel<ServiceHealth[]>("services.health");
if (!services) return <GridSkeleton count={12} />;
return (
<div className="grid grid-cols-4 gap-3">
{services.map((service) => (
<div
key={service.name}
className={`rounded-lg border p-3 ${
service.status === "healthy"
? "border-green-200 bg-green-50"
: service.status === "degraded"
? "border-yellow-200 bg-yellow-50"
: "border-red-200 bg-red-50"
}`}
>
<div className="flex items-center justify-between">
<span className="text-sm font-medium">{service.name}</span>
<StatusBadge status={service.status} />
</div>
<div className="mt-2 grid grid-cols-2 gap-x-4 text-xs text-gray-600">
<div>P99: {service.latencyP99}ms</div>
<div>Errors: {(service.errorRate * 100).toFixed(2)}%</div>
<div>Uptime: {(service.uptime * 100).toFixed(1)}%</div>
</div>
</div>
))}
</div>
);
}Conclusion
Real-time monitoring in React requires careful architecture: WebSocket providers for connection management, channel-based subscriptions for targeted data distribution, buffer management for time-series data, render-rate decoupling from data-rate for performance, and clear connection status indicators. The Web Ops Console combines these patterns to provide Klivvr's operations team with live visibility into every service — ensuring that when something goes wrong, the team sees it immediately and can respond before customers are affected.
Related Articles
Build vs Buy for Internal Operations Tools
A framework for deciding whether to build or buy internal operations tools, covering total cost of ownership, customization needs, and the strategic value of purpose-built tooling.
Improving Incident Response with Ops Dashboards
How operations dashboards improve incident response efficiency through faster detection, structured workflows, MTTR reduction, and postmortem processes.
Data Visualization Patterns for Ops Dashboards
How to choose and implement effective data visualizations for operations dashboards, covering chart selection, color systems, responsive layouts, and accessibility.