Implementing Real-Time Features in React with WebSockets and Server-Sent Events
Real-time features — live notifications, collaborative editing, real-time dashboards, chat — are no longer nice-to-haves. Users expect instant updates without refreshing the page. But choosing the right technology for your real-time needs matters more than you'd think.
WebSockets vs Server-Sent Events: When to Use Which
WebSockets provide full-duplex communication — both client and server can send messages anytime. Use WebSockets when you need:
- Bidirectional communication (chat, collaborative editing)
- Low-latency server-to-client AND client-to-server messaging
- Binary data transfer
Server-Sent Events (SSE) provide one-way communication from server to client over a standard HTTP connection. Use SSE when you need:
- Server-to-client push only (notifications, live feeds, dashboard updates)
- Automatic reconnection (built into the EventSource API)
- Simpler infrastructure (works through HTTP proxies and load balancers)
- Better compatibility with existing REST APIs
Pattern 1: Real-Time Notifications with SSE
// Backend: Express SSE endpoint
app.get('/api/notifications/stream', authenticate, (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
const userId = req.user.id;
// Send heartbeat every 30s to keep connection alive
const heartbeat = setInterval(() => {
res.write(': heartbeat\n\n');
}, 30000);
// Subscribe to user's notification channel
const unsubscribe = notificationBus.subscribe(userId, (notification) => {
res.write(`data: ${JSON.stringify(notification)}\n\n`);
});
req.on('close', () => {
clearInterval(heartbeat);
unsubscribe();
});
});
// Frontend: React hook for SSE notifications
'use client';
import { useEffect, useState, useCallback } from 'react';
export function useNotifications() {
const [notifications, setNotifications] = useState<Notification[]>([]);
useEffect(() => {
const eventSource = new EventSource('/api/notifications/stream');
eventSource.onmessage = (event) => {
const notification = JSON.parse(event.data);
setNotifications(prev => [notification, ...prev].slice(0, 50));
// Optional: Show browser notification
if (Notification.permission === 'granted') {
new Notification(notification.title, { body: notification.message });
}
};
eventSource.onerror = () => {
// EventSource automatically reconnects
console.log('SSE connection lost, reconnecting...');
};
return () => eventSource.close();
}, []);
const markAsRead = useCallback(async (id: string) => {
await fetch(`/api/notifications/${id}/read`, { method: 'PATCH' });
setNotifications(prev =>
prev.map(n => n.id === id ? { ...n, read: true } : n)
);
}, []);
return { notifications, markAsRead };
}
Pattern 2: WebSocket Connection with Auto-Reconnect
// Robust WebSocket hook with exponential backoff
'use client';
import { useEffect, useRef, useState, useCallback } from 'react';
export function useWebSocket(url: string) {
const wsRef = useRef<WebSocket | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [lastMessage, setLastMessage] = useState<unknown>(null);
const retriesRef = useRef(0);
const maxRetries = 10;
const connect = useCallback(() => {
const ws = new WebSocket(url);
wsRef.current = ws;
ws.onopen = () => {
setIsConnected(true);
retriesRef.current = 0; // Reset retries on successful connection
};
ws.onmessage = (event) => {
setLastMessage(JSON.parse(event.data));
};
ws.onclose = () => {
setIsConnected(false);
// Exponential backoff: 1s, 2s, 4s, 8s... up to 30s
if (retriesRef.current < maxRetries) {
const delay = Math.min(1000 * Math.pow(2, retriesRef.current), 30000);
retriesRef.current++;
setTimeout(connect, delay);
}
};
}, [url]);
useEffect(() => {
connect();
return () => wsRef.current?.close();
}, [connect]);
const sendMessage = useCallback((data: unknown) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify(data));
}
}, []);
return { isConnected, lastMessage, sendMessage };
}
Pattern 3: Real-Time Dashboard with Optimistic Updates
For dashboards that display live data, combine SSE for push updates with optimistic UI updates for user actions. This gives users the perception of instant feedback while ensuring data consistency through server confirmation.
Infrastructure Considerations
- Load Balancing: WebSocket connections are stateful — use sticky sessions or a dedicated WebSocket gateway
- Scaling: Use Redis pub/sub to broadcast messages across multiple server instances
- Connection Limits: Monitor open connections — each WebSocket connection consumes server memory
- Mobile: Implement background reconnection handling for mobile browsers that suspend connections
Choose the simplest real-time technology that meets your requirements. SSE handles most server-push scenarios with less complexity than WebSockets. Reserve WebSockets for true bidirectional communication needs.