Skip to main content

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/unsubscribe pattern

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

AspectWebSocketSSE
DirectionBidirectionalServer to client only
Browser supportAll modern browsersAll modern browsers
ComplexityMore (binary frames, heartbeats)Simple (HTTP streaming)
Best forChat, multiplayer gamesDashboards, notifications
Connection costHigher (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.

Further Reading