Back to Blog
Tutorial

Implementing Real-Time Features in React with WebSockets and Server-Sent Events

A practical comparison of WebSockets vs Server-Sent Events for real-time features in React applications. Includes implementation patterns for live notifications, collaborative editing, and real-time dashboards.

UIFlexer TeamDecember 20, 20253 min read
Implementing Real-Time Features in React with WebSockets and Server-Sent Events

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.

Real-time mobile and web application development

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 };
}
Live data streaming and real-time application interfaces

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.

ReactWebSocketSSEreal-timetutorial

Have a similar project in mind?

Let's discuss how we can help build it.

Get in Touch