We use cookies to enhance your experience on the site
CodeWorlds

WebSockets and Server-Sent Events in React

WebSockets and Server-Sent Events (SSE) are two fundamental technologies enabling real-time communication between client and server. In this lesson, we will explore advanced techniques for implementing both solutions in React applications.

WebSockets - Bidirectional Real-Time Communication

Basic WebSocket Implementation

1// hooks/useWebSocket.js
2import { useState, useEffect, useRef, useCallback } from 'react';
3
4const useWebSocket = (url, options = {}) => {
5  const [socket, setSocket] = useState(null);
6  const [lastMessage, setLastMessage] = useState(null);
7  const [readyState, setReadyState] = useState(0);
8  const [connectionError, setConnectionError] = useState(null);
9
10  // Ref for callbacks to avoid re-renders
11  const messageListeners = useRef(new Set());
12  const retryTimeoutRef = useRef(null);
13  const reconnectAttemptsRef = useRef(0);
14
15  const {
16    reconnectAttempts = 5,
17    reconnectInterval = 3000,
18    heartbeatInterval = 30000,
19    onOpen,
20    onClose,
21    onError,
22    onMessage,
23    protocols = []
24  } = options;
25
26  // Function to create connection
27  const connect = useCallback(() => {
28    try {
29      const ws = new WebSocket(url, protocols);
30
31      ws.onopen = (event) => {
32        console.log('WebSocket connected:', url);
33        setReadyState(1); // OPEN
34        setConnectionError(null);
35        reconnectAttemptsRef.current = 0;
36        onOpen?.(event);
37      };
38
39      ws.onmessage = (event) => {
40        let parsedData;
41        try {
42          parsedData = JSON.parse(event.data);
43        } catch {
44          parsedData = event.data;
45        }
46
47        setLastMessage({
48          data: parsedData,
49          timestamp: Date.now(),
50          raw: event.data
51        });
52
53        // Call all registered listeners
54        messageListeners.current.forEach(listener => {
55          listener(parsedData, event);
56        });
57
58        onMessage?.(parsedData, event);
59      };
60
61      ws.onclose = (event) => {
62        console.log('WebSocket disconnected:', event.code, event.reason);
63        setReadyState(3); // CLOSED
64        setSocket(null);
65        onClose?.(event);
66
67        // Automatic reconnection
68        if (!event.wasClean && reconnectAttemptsRef.current < reconnectAttempts) {
69          reconnectAttemptsRef.current++;
70          console.log(`Reconnecting (${reconnectAttemptsRef.current}/${reconnectAttempts})...`);
71
72          retryTimeoutRef.current = setTimeout(() => {
73            connect();
74          }, reconnectInterval * reconnectAttemptsRef.current);
75        }
76      };
77
78      ws.onerror = (error) => {
79        console.error('WebSocket error:', error);
80        setConnectionError(error);
81        onError?.(error);
82      };
83
84      setSocket(ws);
85      setReadyState(0); // CONNECTING
86
87    } catch (error) {
88      console.error('Failed to create WebSocket:', error);
89      setConnectionError(error);
90    }
91  }, [url, protocols, reconnectAttempts, reconnectInterval, onOpen, onClose, onError, onMessage]);
92
93  // Sending messages
94  const sendMessage = useCallback((message) => {
95    if (socket && socket.readyState === WebSocket.OPEN) {
96      const dataToSend = typeof message === 'string' ? message : JSON.stringify(message);
97      socket.send(dataToSend);
98      return true;
99    } else {
100      console.warn('WebSocket is not connected');
101      return false;
102    }
103  }, [socket]);
104
105  // Adding message listener
106  const addMessageListener = useCallback((listener) => {
107    messageListeners.current.add(listener);
108    return () => messageListeners.current.delete(listener);
109  }, []);
110
111  // Manual reconnection
112  const reconnect = useCallback(() => {
113    if (socket) {
114      socket.close();
115    }
116    reconnectAttemptsRef.current = 0;
117    connect();
118  }, [socket, connect]);
119
120  // Disconnect
121  const disconnect = useCallback(() => {
122    if (retryTimeoutRef.current) {
123      clearTimeout(retryTimeoutRef.current);
124    }
125    if (socket) {
126      socket.close(1000, 'Closed by user');
127    }
128  }, [socket]);
129
130  // Automatic connection on mount
131  useEffect(() => {
132    connect();
133
134    return () => {
135      if (retryTimeoutRef.current) {
136        clearTimeout(retryTimeoutRef.current);
137      }
138      if (socket) {
139        socket.close();
140      }
141    };
142  }, [connect]);
143
144  // Heartbeat - ping/pong
145  useEffect(() => {
146    if (socket && readyState === 1 && heartbeatInterval > 0) {
147      const interval = setInterval(() => {
148        if (socket.readyState === WebSocket.OPEN) {
149          sendMessage({ type: 'ping', timestamp: Date.now() });
150        }
151      }, heartbeatInterval);
152
153      return () => clearInterval(interval);
154    }
155  }, [socket, readyState, heartbeatInterval, sendMessage]);
156
157  return {
158    socket,
159    lastMessage,
160    readyState,
161    connectionError,
162    sendMessage,
163    addMessageListener,
164    reconnect,
165    disconnect,
166    isConnecting: readyState === 0,
167    isOpen: readyState === 1,
168    isClosing: readyState === 2,
169    isClosed: readyState === 3
170  };
171};
172
173export default useWebSocket;

Advanced Real-Time Chat

1// components/ChatRoom.jsx
2import React, { useState, useEffect, useRef } from 'react';
3import useWebSocket from '../hooks/useWebSocket';
4
5const ChatRoom = ({ roomId, userId, userName }) => {
6  const [messages, setMessages] = useState([]);
7  const [newMessage, setNewMessage] = useState('');
8  const [typingUsers, setTypingUsers] = useState(new Set());
9  const [onlineUsers, setOnlineUsers] = useState(new Map());
10  const messagesEndRef = useRef(null);
11  const typingTimeoutRef = useRef(null);
12
13  const {
14    sendMessage,
15    lastMessage,
16    isOpen,
17    connectionError,
18    addMessageListener
19  } = useWebSocket(`ws://localhost:8080/chat/${roomId}`, {
20    onOpen: () => {
21      // Join room
22      sendMessage({
23        type: 'join',
24        roomId,
25        userId,
26        userName
27      });
28    }
29  });
30
31  // Handle received messages
32  useEffect(() => {
33    if (!lastMessage) return;
34
35    const { data } = lastMessage;
36
37    switch (data.type) {
38      case 'message':
39        setMessages(prev => [...prev, {
40          id: data.id,
41          text: data.text,
42          userId: data.userId,
43          userName: data.userName,
44          timestamp: data.timestamp
45        }]);
46        break;
47
48      case 'typing':
49        setTypingUsers(prev => {
50          const newSet = new Set(prev);
51          if (data.isTyping && data.userId !== userId) {
52            newSet.add(data.userName);
53          } else {
54            newSet.delete(data.userName);
55          }
56          return newSet;
57        });
58        break;
59
60      case 'userJoined':
61        setOnlineUsers(prev => new Map(prev).set(data.userId, data.userName));
62        setMessages(prev => [...prev, {
63          id: `system-${Date.now()}`,
64          text: `${data.userName} joined the chat`,
65          type: 'system',
66          timestamp: Date.now()
67        }]);
68        break;
69
70      case 'userLeft':
71        setOnlineUsers(prev => {
72          const newMap = new Map(prev);
73          newMap.delete(data.userId);
74          return newMap;
75        });
76        setMessages(prev => [...prev, {
77          id: `system-${Date.now()}`,
78          text: `${data.userName} left the chat`,
79          type: 'system',
80          timestamp: Date.now()
81        }]);
82        break;
83
84      case 'messageDeleted':
85        setMessages(prev => prev.filter(msg => msg.id !== data.messageId));
86        break;
87
88      case 'messageEdited':
89        setMessages(prev => prev.map(msg =>
90          msg.id === data.messageId
91            ? { ...msg, text: data.newText, edited: true }
92            : msg
93        ));
94        break;
95    }
96  }, [lastMessage, userId]);
97
98  // Auto-scroll to latest message
99  useEffect(() => {
100    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
101  }, [messages]);
102
103  // Sending messages
104  const handleSendMessage = (e) => {
105    e.preventDefault();
106
107    if (newMessage.trim() && isOpen) {
108      sendMessage({
109        type: 'message',
110        text: newMessage.trim(),
111        roomId,
112        userId,
113        userName
114      });
115
116      setNewMessage('');
117      handleStopTyping();
118    }
119  };
120
121  // Typing handler
122  const handleTyping = (e) => {
123    setNewMessage(e.target.value);
124
125    if (!typingTimeoutRef.current) {
126      sendMessage({
127        type: 'typing',
128        isTyping: true,
129        userId,
130        userName,
131        roomId
132      });
133    }
134
135    clearTimeout(typingTimeoutRef.current);
136    typingTimeoutRef.current = setTimeout(() => {
137      handleStopTyping();
138    }, 2000);
139  };
140
141  const handleStopTyping = () => {
142    if (typingTimeoutRef.current) {
143      clearTimeout(typingTimeoutRef.current);
144      typingTimeoutRef.current = null;
145
146      sendMessage({
147        type: 'typing',
148        isTyping: false,
149        userId,
150        userName,
151        roomId
152      });
153    }
154  };
155
156  // Delete message
157  const handleDeleteMessage = (messageId) => {
158    sendMessage({
159      type: 'deleteMessage',
160      messageId,
161      userId
162    });
163  };
164
165  if (connectionError) {
166    return (
167      <div className="chat-error">
168        <h3>Chat connection error</h3>
169        <p>{connectionError.message}</p>
170        <button onClick={() => window.location.reload()}>
171          Try again
172        </button>
173      </div>
174    );
175  }
176
177  return (
178    <div className="chat-room">
179      <div className="chat-header">
180        <h2>Room: {roomId}</h2>
181        <div className="online-users">
182          Online ({onlineUsers.size}): {Array.from(onlineUsers.values()).join(', ')}
183        </div>
184        <div className="connection-status">
185          Status: {isOpen ? 'Connected' : 'Disconnected'}
186        </div>
187      </div>
188
189      <div className="chat-messages">
190        {messages.map(message => (
191          <div
192            key={message.id}
193            className={`message ${message.type === 'system' ? 'system' : ''} ${message.userId === userId ? 'own' : ''}`}
194          >
195            {message.type !== 'system' && (
196              <div className="message-header">
197                <span className="username">{message.userName}</span>
198                <span className="timestamp">
199                  {new Date(message.timestamp).toLocaleTimeString()}
200                </span>
201                {message.userId === userId && (
202                  <button
203                    onClick={() => handleDeleteMessage(message.id)}
204                    className="delete-btn"
205                  >
206                    Delete
207                  </button>
208                )}
209              </div>
210            )}
211            <div className="message-text">
212              {message.text}
213              {message.edited && <span className="edited"> (edited)</span>}
214            </div>
215          </div>
216        ))}
217
218        {typingUsers.size > 0 && (
219          <div className="typing-indicator">
220            {Array.from(typingUsers).join(', ')} is typing...
221          </div>
222        )}
223
224        <div ref={messagesEndRef} />
225      </div>
226
227      <form onSubmit={handleSendMessage} className="chat-input">
228        <input
229          type="text"
230          value={newMessage}
231          onChange={handleTyping}
232          onBlur={handleStopTyping}
233            disabled={!isOpen}
234          maxLength={500}
235        />
236        <button type="submit" disabled={!isOpen || !newMessage.trim()}>
237          Send
238        </button>
239      </form>
240    </div>
241  );
242};
243
244export default ChatRoom;

WebSocket with Middleware and Authentication

1// utils/websocketMiddleware.js
2class WebSocketMiddleware {
3  constructor() {
4    this.middlewares = [];
5  }
6
7  use(middleware) {
8    this.middlewares.push(middleware);
9  }
10
11  async processIncoming(message, socket) {
12    let processedMessage = message;
13
14    for (const middleware of this.middlewares) {
15      try {
16        processedMessage = await middleware.incoming(processedMessage, socket);
17        if (!processedMessage) break; // Middleware can stop processing
18      } catch (error) {
19        console.error('Middleware error (incoming):', error);
20        break;
21      }
22    }
23
24    return processedMessage;
25  }
26
27  async processOutgoing(message, socket) {
28    let processedMessage = message;
29
30    for (const middleware of this.middlewares) {
31      try {
32        processedMessage = await middleware.outgoing(processedMessage, socket);
33        if (!processedMessage) break;
34      } catch (error) {
35        console.error('Middleware error (outgoing):', error);
36        break;
37      }
38    }
39
40    return processedMessage;
41  }
42}
43
44// Authentication middleware
45const authMiddleware = {
46  incoming: async (message, socket) => {
47    if (message.type === 'auth') {
48      const token = message.token;
49      const isValid = await validateToken(token);
50
51      if (isValid) {
52        socket.isAuthenticated = true;
53        socket.userId = getUserIdFromToken(token);
54        return { type: 'authSuccess', userId: socket.userId };
55      } else {
56        socket.close(4001, 'Unauthorized');
57        return null;
58      }
59    }
60
61    if (!socket.isAuthenticated && message.type !== 'auth') {
62      socket.send(JSON.stringify({
63        type: 'error',
64        message: 'Authentication required'
65      }));
66      return null;
67    }
68
69    return message;
70  },
71
72  outgoing: async (message, socket) => {
73    if (!socket.isAuthenticated && message.type !== 'authRequired') {
74      return null;
75    }
76    return message;
77  }
78};
79
80// Rate limiting middleware
81const rateLimitMiddleware = {
82  incoming: async (message, socket) => {
83    if (!socket.messageCount) {
84      socket.messageCount = 0;
85      socket.lastReset = Date.now();
86    }
87
88    const now = Date.now();
89    const timePassed = now - socket.lastReset;
90
91    // Reset every minute
92    if (timePassed > 60000) {
93      socket.messageCount = 0;
94      socket.lastReset = now;
95    }
96
97    socket.messageCount++;
98
99    // Maximum 100 messages per minute
100    if (socket.messageCount > 100) {
101      socket.send(JSON.stringify({
102        type: 'error',
103        message: 'Rate limit exceeded'
104      }));
105      return null;
106    }
107
108    return message;
109  },
110
111  outgoing: async (message, socket) => message
112};
113
114// Hook with middleware
115const useAdvancedWebSocket = (url, options = {}) => {
116  const [socket, setSocket] = useState(null);
117  const middleware = useRef(new WebSocketMiddleware());
118
119  useEffect(() => {
120    // Configure middleware
121    middleware.current.use(authMiddleware);
122    middleware.current.use(rateLimitMiddleware);
123  }, []);
124
125  const { sendMessage: baseSendMessage, ...rest } = useWebSocket(url, {
126    ...options,
127    onMessage: async (data, event) => {
128      const processed = await middleware.current.processIncoming(data, event.target);
129      if (processed) {
130        options.onMessage?.(processed, event);
131      }
132    }
133  });
134
135  const sendMessage = useCallback(async (message) => {
136    if (socket) {
137      const processed = await middleware.current.processOutgoing(message, socket);
138      if (processed) {
139        return baseSendMessage(processed);
140      }
141    }
142    return false;
143  }, [socket, baseSendMessage]);
144
145  return {
146    ...rest,
147    sendMessage,
148    socket
149  };
150};

Server-Sent Events (SSE) - Unidirectional Communication

Basic SSE Implementation

1// hooks/useServerSentEvents.js
2import { useState, useEffect, useRef, useCallback } from 'react';
3
4const useServerSentEvents = (url, options = {}) => {
5  const [data, setData] = useState(null);
6  const [readyState, setReadyState] = useState(0);
7  const [error, setError] = useState(null);
8  const eventSourceRef = useRef(null);
9  const listenersRef = useRef(new Map());
10
11  const {
12    withCredentials = false,
13    retryTime = 3000,
14    maxRetries = 5,
15    onOpen,
16    onError,
17    onMessage
18  } = options;
19
20  const connect = useCallback(() => {
21    try {
22      const eventSource = new EventSource(url, { withCredentials });
23      eventSourceRef.current = eventSource;
24
25      eventSource.onopen = (event) => {
26        console.log('SSE connected:', url);
27        setReadyState(1); // OPEN
28        setError(null);
29        onOpen?.(event);
30      };
31
32      eventSource.onmessage = (event) => {
33        let parsedData;
34        try {
35          parsedData = JSON.parse(event.data);
36        } catch {
37          parsedData = event.data;
38        }
39
40        setData({
41          data: parsedData,
42          timestamp: Date.now(),
43          id: event.lastEventId,
44          type: event.type
45        });
46
47        onMessage?.(parsedData, event);
48      };
49
50      eventSource.onerror = (event) => {
51        console.error('SSE error:', event);
52        setReadyState(2); // CLOSED
53        setError(event);
54        onError?.(event);
55      };
56
57      // Add custom event listeners
58      listenersRef.current.forEach((listener, eventType) => {
59        eventSource.addEventListener(eventType, listener);
60      });
61
62    } catch (err) {
63      console.error('Failed to create EventSource:', err);
64      setError(err);
65    }
66  }, [url, withCredentials, onOpen, onMessage, onError]);
67
68  const addEventListener = useCallback((eventType, listener) => {
69    listenersRef.current.set(eventType, listener);
70
71    if (eventSourceRef.current) {
72      eventSourceRef.current.addEventListener(eventType, listener);
73    }
74
75    return () => {
76      listenersRef.current.delete(eventType);
77      if (eventSourceRef.current) {
78        eventSourceRef.current.removeEventListener(eventType, listener);
79      }
80    };
81  }, []);
82
83  const close = useCallback(() => {
84    if (eventSourceRef.current) {
85      eventSourceRef.current.close();
86      eventSourceRef.current = null;
87      setReadyState(2);
88    }
89  }, []);
90
91  useEffect(() => {
92    connect();
93
94    return () => {
95      close();
96    };
97  }, [connect, close]);
98
99  return {
100    data,
101    readyState,
102    error,
103    addEventListener,
104    close,
105    reconnect: connect,
106    isConnecting: readyState === 0,
107    isOpen: readyState === 1,
108    isClosed: readyState === 2
109  };
110};
111
112export default useServerSentEvents;

Live Dashboard with SSE

1// components/LiveDashboard.jsx
2import React, { useState, useEffect } from 'react';
3import useServerSentEvents from '../hooks/useServerSentEvents';
4
5const LiveDashboard = () => {
6  const [metrics, setMetrics] = useState({
7    activeUsers: 0,
8    serverLoad: 0,
9    memoryUsage: 0,
10    requestsPerSecond: 0
11  });
12
13  const [notifications, setNotifications] = useState([]);
14  const [systemStatus, setSystemStatus] = useState('healthy');
15  const [lastUpdate, setLastUpdate] = useState(null);
16
17  const { data, isOpen, error, addEventListener } = useServerSentEvents(
18    '/api/dashboard/stream',
19    {
20      onMessage: (data) => {
21        setLastUpdate(new Date());
22      }
23    }
24  );
25
26  // Handler for different event types
27  useEffect(() => {
28    const removeMetricsListener = addEventListener('metrics', (event) => {
29      const metricsData = JSON.parse(event.data);
30      setMetrics(metricsData);
31    });
32
33    const removeNotificationListener = addEventListener('notification', (event) => {
34      const notification = JSON.parse(event.data);
35      setNotifications(prev => [notification, ...prev.slice(0, 9)]); // Maximum 10 notifications
36    });
37
38    const removeStatusListener = addEventListener('status', (event) => {
39      const status = JSON.parse(event.data);
40      setSystemStatus(status.level);
41    });
42
43    const removeAlertListener = addEventListener('alert', (event) => {
44      const alert = JSON.parse(event.data);
45
46      // Show alert as browser notification
47      if (Notification.permission === 'granted') {
48        new Notification(`System Alert: ${alert.type}`, {
49          body: alert.message,
50          icon: '/icons/alert.png'
51        });
52      }
53
54      setNotifications(prev => [{
55        ...alert,
56        type: 'alert',
57        timestamp: Date.now()
58      }, ...prev.slice(0, 9)]);
59    });
60
61    return () => {
62      removeMetricsListener();
63      removeNotificationListener();
64      removeStatusListener();
65      removeAlertListener();
66    };
67  }, [addEventListener]);
68
69  // Request notification permission
70  useEffect(() => {
71    if (Notification.permission === 'default') {
72      Notification.requestPermission();
73    }
74  }, []);
75
76  const getStatusColor = (status) => {
77    switch (status) {
78      case 'healthy': return '#4CAF50';
79      case 'warning': return '#FF9800';
80      case 'critical': return '#F44336';
81      default: return '#9E9E9E';
82    }
83  };
84
85  const getMetricColor = (value, thresholds) => {
86    if (value >= thresholds.critical) return '#F44336';
87    if (value >= thresholds.warning) return '#FF9800';
88    return '#4CAF50';
89  };
90
91  if (error) {
92    return (
93      <div className="dashboard-error">
94        <h2>Dashboard connection error</h2>
95        <p>Unable to establish connection to the monitoring server.</p>
96        <button onClick={() => window.location.reload()}>
97          Refresh page
98        </button>
99      </div>
100    );
101  }
102
103  return (
104    <div className="live-dashboard">
105      <header className="dashboard-header">
106        <h1>Live Dashboard</h1>
107        <div className="connection-info">
108          <span
109            className={`status-indicator ${isOpen ? 'connected' : 'disconnected'}`}
110            style={{ backgroundColor: isOpen ? '#4CAF50' : '#F44336' }}
111          />
112          {isOpen ? 'Connected' : 'Disconnected'}
113          {lastUpdate && (
114            <span className="last-update">
115              Last update: {lastUpdate.toLocaleTimeString()}
116            </span>
117          )}
118        </div>
119      </header>
120
121      <div className="metrics-grid">
122        <div className="metric-card">
123          <h3>Active Users</h3>
124          <div
125            className="metric-value"
126            style={{ color: getMetricColor(metrics.activeUsers, { warning: 1000, critical: 1500 }) }}
127          >
128            {metrics.activeUsers.toLocaleString()}
129          </div>
130        </div>
131
132        <div className="metric-card">
133          <h3>Server Load</h3>
134          <div
135            className="metric-value"
136            style={{ color: getMetricColor(metrics.serverLoad, { warning: 70, critical: 90 }) }}
137          >
138            {metrics.serverLoad}%
139          </div>
140          <div className="progress-bar">
141            <div
142              className="progress-fill"
143              style={{
144                width: `${metrics.serverLoad}%`,
145                backgroundColor: getMetricColor(metrics.serverLoad, { warning: 70, critical: 90 })
146              }}
147            />
148          </div>
149        </div>
150
151        <div className="metric-card">
152          <h3>Memory Usage</h3>
153          <div
154            className="metric-value"
155            style={{ color: getMetricColor(metrics.memoryUsage, { warning: 80, critical: 95 }) }}
156          >
157            {metrics.memoryUsage}%
158          </div>
159          <div className="progress-bar">
160            <div
161              className="progress-fill"
162              style={{
163                width: `${metrics.memoryUsage}%`,
164                backgroundColor: getMetricColor(metrics.memoryUsage, { warning: 80, critical: 95 })
165              }}
166            />
167          </div>
168        </div>
169
170        <div className="metric-card">
171          <h3>Requests/sec</h3>
172          <div
173            className="metric-value"
174            style={{ color: getMetricColor(metrics.requestsPerSecond, { warning: 1000, critical: 2000 }) }}
175          >
176            {metrics.requestsPerSecond}
177          </div>
178        </div>
179      </div>
180
181      <div className="system-status">
182        <h3>System Status</h3>
183        <div
184          className="status-badge"
185          style={{
186            backgroundColor: getStatusColor(systemStatus),
187            color: 'white',
188            padding: '8px 16px',
189            borderRadius: '20px',
190            display: 'inline-block'
191          }}
192        >
193          {systemStatus.toUpperCase()}
194        </div>
195      </div>
196
197      <div className="notifications-panel">
198        <h3>Notifications ({notifications.length})</h3>
199        <div className="notifications-list">
200          {notifications.map((notification, index) => (
201            <div
202              key={index}
203              className={`notification ${notification.type || 'info'}`}
204            >
205              <div className="notification-content">
206                <strong>{notification.title || notification.type}</strong>
207                <p>{notification.message}</p>
208              </div>
209              <div className="notification-time">
210                {new Date(notification.timestamp).toLocaleTimeString()}
211              </div>
212            </div>
213          ))}
214        </div>
215      </div>
216    </div>
217  );
218};
219
220export default LiveDashboard;

Choosing Between WebSockets and SSE

When to Use WebSockets

1// Example: Real-time collaboration application
2const CollaborativeEditor = () => {
3  const { sendMessage, lastMessage, isOpen } = useWebSocket('/ws/document/123');
4  const [documentContent, setDocumentContent] = useState('');
5  const [cursors, setCursors] = useState(new Map());
6
7  const handleTextChange = (newContent, position) => {
8    setDocumentContent(newContent);
9
10    // Send change to other users
11    sendMessage({
12      type: 'content_change',
13      content: newContent,
14      position,
15      userId: currentUser.id
16    });
17  };
18
19  const handleCursorMove = (position) => {
20    sendMessage({
21      type: 'cursor_move',
22      position,
23      userId: currentUser.id
24    });
25  };
26
27  useEffect(() => {
28    if (lastMessage) {
29      switch (lastMessage.data.type) {
30        case 'content_change':
31          setDocumentContent(lastMessage.data.content);
32          break;
33        case 'cursor_move':
34          setCursors(prev => new Map(prev).set(
35            lastMessage.data.userId,
36            lastMessage.data.position
37          ));
38          break;
39      }
40    }
41  }, [lastMessage]);
42
43  return (
44    <div className="collaborative-editor">
45      {/* Editor with real-time collaboration */}
46    </div>
47  );
48};

When to Use SSE

1// Example: Live news feed
2const NewsFeed = () => {
3  const [articles, setArticles] = useState([]);
4  const [newArticleCount, setNewArticleCount] = useState(0);
5
6  const { data, addEventListener } = useServerSentEvents('/api/news/stream');
7
8  useEffect(() => {
9    const removeArticleListener = addEventListener('new_article', (event) => {
10      const article = JSON.parse(event.data);
11      setArticles(prev => [article, ...prev]);
12      setNewArticleCount(prev => prev + 1);
13    });
14
15    const removeUpdateListener = addEventListener('article_update', (event) => {
16      const updatedArticle = JSON.parse(event.data);
17      setArticles(prev => prev.map(article =>
18        article.id === updatedArticle.id ? updatedArticle : article
19      ));
20    });
21
22    return () => {
23      removeArticleListener();
24      removeUpdateListener();
25    };
26  }, [addEventListener]);
27
28  return (
29    <div className="news-feed">
30      {newArticleCount > 0 && (
31        <div className="new-articles-banner">
32          {newArticleCount} new articles
33          <button onClick={() => setNewArticleCount(0)}>
34            Mark as read
35          </button>
36        </div>
37      )}
38
39      {articles.map(article => (
40        <ArticleCard key={article.id} article={article} />
41      ))}
42    </div>
43  );
44};

Comparison and Best Practices

1// Utility for choosing the right technology
2const useRealTimeConnection = (url, options = {}) => {
3  const {
4    bidirectional = false,
5    authentication = false,
6    highFrequency = false,
7    binaryData = false
8  } = options;
9
10  // Technology decision based on requirements
11  const shouldUseWebSocket =
12    bidirectional ||
13    authentication ||
14    highFrequency ||
15    binaryData;
16
17  if (shouldUseWebSocket) {
18    return {
19      type: 'websocket',
20      connection: useWebSocket(url.replace('http', 'ws'), options)
21    };
22  } else {
23    return {
24      type: 'sse',
25      connection: useServerSentEvents(url, options)
26    };
27  }
28};
29
30// Usage example
31const SmartComponent = () => {
32  const { type, connection } = useRealTimeConnection('/api/data', {
33    bidirectional: true, // Requires WebSocket
34    authentication: true
35  });
36
37  if (type === 'websocket') {
38    const { sendMessage, lastMessage } = connection;
39    // WebSocket logic
40  } else {
41    const { data, addEventListener } = connection;
42    // SSE logic
43  }
44
45  return <div>Smart real-time component</div>;
46};

WebSockets and Server-Sent Events are complementary technologies for real-time communication. WebSockets offer full bidirectional communication and are ideal for applications requiring intensive interaction, while SSE provides simpler, unidirectional data streaming from server to client with automatic reconnection.

Go to CodeWorlds