Real-Time Data Updates in React Dashboards
A dashboard polling the API every 5 seconds for updates is not scalable. If 1,000 users refresh data every 5 seconds, that's 12,000 requests per minute. WebSockets, which maintain a persistent connection and push updates from server to client, eliminate polling and reduce server load by 60–80% while improving latency from 5,000ms (polling interval) to 100ms (network speed).
I built a live transaction dashboard last year that started with polling and melted under load. Switching to WebSockets cut server costs by 40% and made the dashboard feel responsive. This article teaches you both WebSocket and Server-Sent Events (SSE) patterns.
What You'll Learn
You'll build:
- A WebSocket connection that persists across component lifecycle
- A custom hook for subscribing to real-time data
- Automatic reconnection when the connection drops
- SSE (Server-Sent Events) as a simpler alternative to WebSockets
- Real-time chart updates driven by server pushes
Prerequisites
You need the dashboard and charting setup from earlier articles. Understanding of TCP/IP and the WebSocket handshake is helpful but not essential.
Step 1: Create a WebSocket Connection Manager
Create src/lib/websocket.ts:
type MessageHandler = (data: unknown) => void;
class WebSocketManager {
private ws: WebSocket | null = null;
private url: string;
private handlers: Map<string, Set<MessageHandler>> = new Map();
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private reconnectDelay = 1000;
constructor(url: string) {
this.url = url;
}
connect(): Promise<void> {
return new Promise((resolve, reject) => {
try {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.reconnectAttempts = 0;
this.reconnectDelay = 1000;
resolve();
};
this.ws.onmessage = (event) => {
try {
const { type, payload } = JSON.parse(event.data);
const listeners = this.handlers.get(type) || new Set();
listeners.forEach((handler) => handler(payload));
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};
this.ws.onclose = () => {
console.log('WebSocket disconnected');
this.attemptReconnect();
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
reject(error);
};
} catch (error) {
reject(error);
}
});
}
private attemptReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
console.log(`Reconnecting in ${delay}ms...`);
setTimeout(() => this.connect().catch(console.error), delay);
}
}
subscribe(type: string, handler: MessageHandler) {
if (!this.handlers.has(type)) {
this.handlers.set(type, new Set());
}
this.handlers.get(type)!.add(handler);
// Return unsubscribe function
return () => {
const listeners = this.handlers.get(type);
if (listeners) {
listeners.delete(handler);
}
};
}
send(type: string, payload: unknown) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type, payload }));
}
}
close() {
if (this.ws) {
this.ws.close();
}
}
}
export const wsManager = new WebSocketManager(
import.meta.env.VITE_WS_URL || `wss://${window.location.host}/ws`
);
This manager:
- Connects and reconnects with exponential backoff (1s, 2s, 4s, etc.)
- Routes incoming messages by type to registered handlers
- Provides a
subscribe/unsubscribepattern
Step 2: Create a Hook for Real-Time Data
Create src/hooks/useRealtimeData.ts:
import { useEffect, useState } from 'react';
import { wsManager } from '../lib/websocket';
interface UseRealtimeDataOptions<T> {
messageType: string;
initialData?: T;
}
export function useRealtimeData<T>(options: UseRealtimeDataOptions<T>) {
const [data, setData] = useState(options.initialData);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
// Ensure WebSocket is connected
wsManager.connect().then(() => setIsConnected(true)).catch(console.error);
// Subscribe to the message type
const unsubscribe = wsManager.subscribe(options.messageType, (payload) => {
setData(payload as T);
});
// Cleanup
return () => {
unsubscribe();
};
}, [options.messageType]);
return { data, isConnected };
}
Any component can now use useRealtimeData('revenue-update') to receive live updates.
Step 3: Update Charts with Real-Time Data
Create src/components/LiveRevenueChart.tsx:
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from 'recharts';
import { useRealtimeData } from '../hooks/useRealtimeData';
import { useQuery } from '@tanstack/react-query';
interface RevenuePoint {
timestamp: string;
revenue: number;
}
export function LiveRevenueChart() {
// Fetch initial data
const { data: historicalData, isLoading } = useQuery<RevenuePoint[]>({
queryKey: ['revenue-historical'],
queryFn: async () => {
const response = await fetch('/api/revenue-historical');
return response.json();
},
});
// Subscribe to real-time updates
const { data: liveUpdate } = useRealtimeData<RevenuePoint>({
messageType: 'revenue-update',
});
// Combine historical and real-time data
const chartData = [...(historicalData || [])];
if (liveUpdate) {
chartData.push(liveUpdate);
}
if (isLoading) return <p>Loading chart...</p>;
return (
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-lg font-semibold mb-4">Live Revenue</h2>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="timestamp" />
<YAxis />
<Tooltip />
<Line
type="monotone"
dataKey="revenue"
stroke="#3b82f6"
strokeWidth={2}
isAnimationActive={true}
/>
</LineChart>
</ResponsiveContainer>
</div>
);
}
The chart automatically re-renders when liveUpdate arrives, and Recharts animates the line smoothly to the new point.
Step 4: Implement Server-Sent Events (SSE) as an Alternative
SSE is simpler than WebSockets for one-way server-to-client communication. Create src/hooks/useSSE.ts:
import { useEffect, useState } from 'react';
interface UseSSEOptions<T> {
url: string;
initialData?: T;
}
export function useSSE<T>(options: UseSSEOptions<T>) {
const [data, setData] = useState(options.initialData);
const [error, setError] = useState<string | null>(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const eventSource = new EventSource(options.url);
eventSource.onopen = () => {
setIsConnected(true);
setError(null);
};
eventSource.onmessage = (event) => {
try {
const payload = JSON.parse(event.data);
setData(payload as T);
} catch (error) {
console.error('Failed to parse SSE message:', error);
}
};
eventSource.onerror = () => {
setIsConnected(false);
setError('Connection lost. Reconnecting...');
// EventSource auto-reconnects after 1s
};
return () => {
eventSource.close();
};
}, [options.url]);
return { data, isConnected, error };
}
Use it like:
const { data: liveMetrics } = useSSE({
url: '/api/metrics/stream',
initialData: { users: 0, active: 0 },
});
return <p>Active users: {liveMetrics.active}</p>;
WebSockets vs. SSE Comparison
| Aspect | WebSocket | SSE |
|---|---|---|
| Direction | Bidirectional | Server to client only |
| Browser support | All modern browsers | All modern browsers |
| Complexity | More (binary frames, heartbeats) | Simple (HTTP streaming) |
| Best for | Chat, multiplayer games | Dashboards, notifications |
| Connection cost | Higher (persistent TCP) | Lower (HTTP semantics) |
For most SaaS dashboards, SSE is sufficient and simpler. Use WebSockets if you need client-to-server real-time communication.
Key Takeaways
- WebSockets reduce server load by eliminating polling; a 1,000-user dashboard refreshes 12,000 times per minute with polling, zero times with WebSockets.
- Implement automatic reconnection with exponential backoff to handle network glitches gracefully.
- Real-time data updates trigger component re-renders automatically when using React state—Recharts animates changes smoothly.
- SSE is a lightweight alternative for server-to-client-only scenarios; it uses standard HTTP and is easier to proxy.
- Always show connection status (
isConnected) so users know when data is stale.
Frequently Asked Questions
What if the WebSocket connection drops mid-request?
The useRealtimeData hook includes automatic reconnection logic. The connection will re-establish within a few seconds, and subsequent updates flow normally. The chart won't flicker; it just stops updating until reconnected.
Can I send data to the server over WebSocket?
Yes, use wsManager.send('event-name', payload). For example, wsManager.send('filter-change', { status: 'active' }) tells the server you've filtered, and it can push back only relevant updates.
Does SSE support compression?
Yes, if the server sends gzip-compressed data, the browser decompresses it automatically. WebSockets require manual compression with a library like brotli-wasm.
How do I test real-time features?
Mock the WebSocket: jest.mock('../lib/websocket', () => ({ wsManager: { subscribe: jest.fn() } })). Or use a library like ws to run a test WebSocket server. For SSE, mock EventSource.
What about firewalls blocking WebSockets?
Some corporate proxies block WebSocket. Implement a fallback: try WebSocket first, then fall back to SSE, then polling. Libraries like Socket.io handle this automatically.