Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

WebSockets i Server-Sent Events w React

WebSockets i Server-Sent Events (SSE) to dwie fundamentalne technologie umożliwiające komunikację w czasie rzeczywistym między klientem a serwerem. W tej lekcji poznamy zaawansowane techniki implementacji obu rozwiązań w aplikacjach React.

WebSockets - dwukierunkowa komunikacja w czasie rzeczywistym

Podstawowa implementacja WebSocket

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 dla callbacks aby uniknąć re-renderów
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  // Funkcja do tworzenia połączenia
27  const connect = useCallback(() => {
28    try {
29      const ws = new WebSocket(url, protocols);
30      
31      ws.onopen = (event) => {
32        console.log('WebSocket połączony:', 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        // Wywołaj wszystkie zarejestrowane listenery
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 rozłączony:', event.code, event.reason);
63        setReadyState(3); // CLOSED
64        setSocket(null);
65        onClose?.(event);
66        
67        // Automatyczne ponowne łączenie
68        if (!event.wasClean && reconnectAttemptsRef.current < reconnectAttempts) {
69          reconnectAttemptsRef.current++;
70          console.log(`Ponowne łączenie (${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 błąd:', error);
80        setConnectionError(error);
81        onError?.(error);
82      };
83      
84      setSocket(ws);
85      setReadyState(0); // CONNECTING
86      
87    } catch (error) {
88      console.error('Nie udało się utworzyć WebSocket:', error);
89      setConnectionError(error);
90    }
91  }, [url, protocols, reconnectAttempts, reconnectInterval, onOpen, onClose, onError, onMessage]);
92
93  // Wysyłanie wiadomości
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 nie jest połączony');
101      return false;
102    }
103  }, [socket]);
104
105  // Dodawanie listenera wiadomości
106  const addMessageListener = useCallback((listener) => {
107    messageListeners.current.add(listener);
108    return () => messageListeners.current.delete(listener);
109  }, []);
110
111  // Ręczne połączenie
112  const reconnect = useCallback(() => {
113    if (socket) {
114      socket.close();
115    }
116    reconnectAttemptsRef.current = 0;
117    connect();
118  }, [socket, connect]);
119
120  // Rozłączenie
121  const disconnect = useCallback(() => {
122    if (retryTimeoutRef.current) {
123      clearTimeout(retryTimeoutRef.current);
124    }
125    if (socket) {
126      socket.close(1000, 'Zamknięte przez użytkownika');
127    }
128  }, [socket]);
129
130  // Automatyczne połączenie przy 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;

Zaawansowany chat w czasie rzeczywistym

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      // Dołącz do pokoju
22      sendMessage({
23        type: 'join',
24        roomId,
25        userId,
26        userName
27      });
28    }
29  });
30
31  // Obsługa otrzymanych wiadomości
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} dołączył do czatu`,
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} opuścił czat`,
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 do najnowszej wiadomości
99  useEffect(() => {
100    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
101  }, [messages]);
102
103  // Wysyłanie wiadomości
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  // Obsługa pisania
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  // Usuwanie wiadomości
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>Błąd połączenia z czatem</h3>
169        <p>{connectionError.message}</p>
170        <button onClick={() => window.location.reload()}>
171          Spróbuj ponownie
172        </button>
173      </div>
174    );
175  }
176
177  return (
178    <div className="chat-room">
179      <div className="chat-header">
180        <h2>Pokój: {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 ? '🟢 Połączony' : '🔴 Rozłączony'}
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                    🗑️
207                  </button>
208                )}
209              </div>
210            )}
211            <div className="message-text">
212              {message.text}
213              {message.edited && <span className="edited"> (edytowane)</span>}
214            </div>
215          </div>
216        ))}
217        
218        {typingUsers.size > 0 && (
219          <div className="typing-indicator">
220            {Array.from(typingUsers).join(', ')} pisze...
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          Wyślij
238        </button>
239      </form>
240    </div>
241  );
242};
243
244export default ChatRoom;

WebSocket z middleware i 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 może zatrzymać przetwarzanie
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// Middleware do autentykacji
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// Middleware do rate limiting
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 co minutę
92    if (timePassed > 60000) {
93      socket.messageCount = 0;
94      socket.lastReset = now;
95    }
96    
97    socket.messageCount++;
98    
99    // Maksymalnie 100 wiadomości na minutę
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 z middleware
115const useAdvancedWebSocket = (url, options = {}) => {
116  const [socket, setSocket] = useState(null);
117  const middleware = useRef(new WebSocketMiddleware());
118  
119  useEffect(() => {
120    // Konfiguracja 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) - jednostronna komunikacja

Podstawowa implementacja SSE

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 połączone:', 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 błąd:', event);
52        setReadyState(2); // CLOSED
53        setError(event);
54        onError?.(event);
55      };
56      
57      // Dodaj custom event listeners
58      listenersRef.current.forEach((listener, eventType) => {
59        eventSource.addEventListener(eventType, listener);
60      });
61      
62    } catch (err) {
63      console.error('Nie udało się utworzyć 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 z 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 dla różnych typów eventów
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)]); // Maksymalnie 10 notyfikacji
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      // Pokazuj alert jako 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>Błąd połączenia z dashboard</h2>
95        <p>Nie można nawiązać połączenia z serwerem monitoringu.</p>
96        <button onClick={() => window.location.reload()}>
97          Odśwież stronę
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 ? 'Połączony' : 'Rozłączony'}
113          {lastUpdate && (
114            <span className="last-update">
115              Ostatnia aktualizacja: {lastUpdate.toLocaleTimeString()}
116            </span>
117          )}
118        </div>
119      </header>
120
121      <div className="metrics-grid">
122        <div className="metric-card">
123          <h3>Aktywni użytkownicy</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>Obciążenie serwera</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>Użycie pamięci</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>Żądania/sek</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>Status systemu</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>Powiadomienia ({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;

Wybór między WebSockets a SSE

Kiedy używać WebSockets

1// Przykład: Aplikacja do kolaboracji w czasie rzeczywistym
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    // Wyślij zmianę do innych użytkowników
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      {/* Edytor z real-time collaboration */}
46    </div>
47  );
48};

Kiedy używać SSE

1// Przykład: Live feed aktualności
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} nowych artykułów
33          <button onClick={() => setNewArticleCount(0)}>
34            Oznacz jako przeczytane
35          </button>
36        </div>
37      )}
38      
39      {articles.map(article => (
40        <ArticleCard key={article.id} article={article} />
41      ))}
42    </div>
43  );
44};

Porównanie i najlepsze praktyki

1// Utility do wyboru odpowiedniej technologii
2const useRealTimeConnection = (url, options = {}) => {
3  const { 
4    bidirectional = false,
5    authentication = false,
6    highFrequency = false,
7    binaryData = false
8  } = options;
9
10  // Decyzja o technologii na podstawie wymagań
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// Przykład użycia
31const SmartComponent = () => {
32  const { type, connection } = useRealTimeConnection('/api/data', {
33    bidirectional: true, // Wymaga WebSocket
34    authentication: true
35  });
36
37  if (type === 'websocket') {
38    const { sendMessage, lastMessage } = connection;
39    // Logika dla WebSocket
40  } else {
41    const { data, addEventListener } = connection;
42    // Logika dla SSE
43  }
44
45  return <div>Smart real-time component</div>;
46};

WebSockets i Server-Sent Events to komplementarne technologie dla komunikacji w czasie rzeczywistym. WebSockets oferują pełną dwukierunkową komunikację i są idealne dla aplikacji wymagających intensywnej interakcji, podczas gdy SSE zapewniają prostsze, jednokierunkowe strumieniowanie danych od serwera do klienta z automatycznym ponownym łączeniem.

Ir a CodeWorlds