Skip to main content
{/}codefunc
GuidesJuly 7, 2026 · 8 min read

WebSockets and Real-Time Data: A Frontend Developer's Guide

Learn to implement real-time features with WebSockets and Server-Sent Events — chat apps, live notifications, collaborative editing, and handling connection state in React.

By codefunc

websocketsreal-timessereactfrontendsocket-io

Real-time data makes apps feel alive — chat messages appear instantly, notifications pop up without refresh, and collaborative documents update as teammates type. WebSockets enable bidirectional communication between browser and server. Server-Sent Events (SSE) provide simpler one-way streaming.

Bottom line: Use WebSockets for bidirectional real-time communication (chat, multiplayer). Use SSE for server-to-client streaming (notifications, feeds). Handle connection lifecycle carefully — reconnect on disconnect, queue messages when offline. Libraries like Socket.IO simplify reconnection and fallbacks, but add overhead. Native WebSocket API works well for simple cases.

WebSockets vs SSE vs Polling

Feature WebSockets SSE Polling
Direction Bidirectional Server → Client Client → Server
Connection Persistent Persistent Repeated requests
Overhead Low Low High
Browser support All modern All modern All
Auto-reconnect Manual Built-in N/A
Binary data Yes No (text only) Yes
Best for Chat, games Notifications, feeds Simple updates

Native WebSocket API

Basic connection

const socket = new WebSocket('wss://api.example.com/ws');

// Connection opened
socket.addEventListener('open', (event) => {
  console.log('Connected');
  socket.send(JSON.stringify({ type: 'subscribe', channel: 'updates' }));
});

// Receive messages
socket.addEventListener('message', (event) => {
  const data = JSON.parse(event.data);
  console.log('Received:', data);
});

// Handle errors
socket.addEventListener('error', (event) => {
  console.error('WebSocket error:', event);
});

// Connection closed
socket.addEventListener('close', (event) => {
  console.log('Disconnected:', event.code, event.reason);
});

Sending messages

// Send JSON
socket.send(JSON.stringify({
  type: 'message',
  content: 'Hello!',
  timestamp: Date.now(),
}));

// Check ready state before sending
if (socket.readyState === WebSocket.OPEN) {
  socket.send(data);
}

Ready states

State Value Meaning
CONNECTING 0 Connection in progress
OPEN 1 Connected and ready
CLOSING 2 Closing handshake
CLOSED 3 Connection closed

React WebSocket hook

import { useEffect, useRef, useState, useCallback } from 'react';

interface UseWebSocketOptions {
  onMessage?: (data: any) => void;
  onOpen?: () => void;
  onClose?: () => void;
  onError?: (error: Event) => void;
  reconnect?: boolean;
  reconnectInterval?: number;
}

function useWebSocket(url: string, options: UseWebSocketOptions = {}) {
  const {
    onMessage,
    onOpen,
    onClose,
    onError,
    reconnect = true,
    reconnectInterval = 3000,
  } = options;

  const socketRef = useRef<WebSocket | null>(null);
  const [isConnected, setIsConnected] = useState(false);
  const reconnectTimeoutRef = useRef<NodeJS.Timeout>();

  const connect = useCallback(() => {
    const socket = new WebSocket(url);
    socketRef.current = socket;

    socket.onopen = () => {
      setIsConnected(true);
      onOpen?.();
    };

    socket.onmessage = (event) => {
      const data = JSON.parse(event.data);
      onMessage?.(data);
    };

    socket.onclose = () => {
      setIsConnected(false);
      onClose?.();
      
      if (reconnect) {
        reconnectTimeoutRef.current = setTimeout(connect, reconnectInterval);
      }
    };

    socket.onerror = (error) => {
      onError?.(error);
    };
  }, [url, onMessage, onOpen, onClose, onError, reconnect, reconnectInterval]);

  useEffect(() => {
    connect();

    return () => {
      clearTimeout(reconnectTimeoutRef.current);
      socketRef.current?.close();
    };
  }, [connect]);

  const send = useCallback((data: any) => {
    if (socketRef.current?.readyState === WebSocket.OPEN) {
      socketRef.current.send(JSON.stringify(data));
    }
  }, []);

  return { isConnected, send };
}

Using the hook

function Chat() {
  const [messages, setMessages] = useState<Message[]>([]);
  
  const { isConnected, send } = useWebSocket('wss://api.example.com/chat', {
    onMessage: (data) => {
      if (data.type === 'message') {
        setMessages((prev) => [...prev, data.message]);
      }
    },
    onOpen: () => {
      send({ type: 'join', room: 'general' });
    },
  });

  const sendMessage = (content: string) => {
    send({ type: 'message', content });
  };

  return (
    <div>
      <div className={isConnected ? 'connected' : 'disconnected'}>
        {isConnected ? '🟢 Connected' : '🔴 Disconnected'}
      </div>
      <MessageList messages={messages} />
      <MessageInput onSend={sendMessage} disabled={!isConnected} />
    </div>
  );
}

Socket.IO

Socket.IO adds features like automatic reconnection, rooms, and fallbacks.

Setup

npm install socket.io-client
import { io, Socket } from 'socket.io-client';

const socket = io('https://api.example.com', {
  autoConnect: false,
  auth: {
    token: getAuthToken(),
  },
});

// Connect
socket.connect();

// Events
socket.on('connect', () => {
  console.log('Connected:', socket.id);
});

socket.on('disconnect', (reason) => {
  console.log('Disconnected:', reason);
});

socket.on('message', (data) => {
  console.log('Message:', data);
});

// Emit events
socket.emit('join', { room: 'general' });
socket.emit('message', { content: 'Hello!' });

// With acknowledgment
socket.emit('message', { content: 'Hello!' }, (response) => {
  console.log('Server acknowledged:', response);
});

React context for Socket.IO

import { createContext, useContext, useEffect, useState } from 'react';
import { io, Socket } from 'socket.io-client';

const SocketContext = createContext<Socket | null>(null);

export function SocketProvider({ children }: { children: React.ReactNode }) {
  const [socket, setSocket] = useState<Socket | null>(null);

  useEffect(() => {
    const newSocket = io('https://api.example.com', {
      auth: { token: getAuthToken() },
    });
    
    setSocket(newSocket);

    return () => {
      newSocket.close();
    };
  }, []);

  return (
    <SocketContext.Provider value={socket}>
      {children}
    </SocketContext.Provider>
  );
}

export function useSocket() {
  const socket = useContext(SocketContext);
  if (!socket) {
    throw new Error('useSocket must be used within SocketProvider');
  }
  return socket;
}
function Notifications() {
  const socket = useSocket();
  const [notifications, setNotifications] = useState<Notification[]>([]);

  useEffect(() => {
    socket.on('notification', (data) => {
      setNotifications((prev) => [data, ...prev]);
    });

    return () => {
      socket.off('notification');
    };
  }, [socket]);

  return <NotificationList items={notifications} />;
}

Server-Sent Events (SSE)

SSE is simpler than WebSockets for one-way server-to-client streaming.

Basic SSE

const eventSource = new EventSource('/api/events');

eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('Event:', data);
};

eventSource.onerror = (error) => {
  console.error('SSE error:', error);
};

// Named events
eventSource.addEventListener('notification', (event) => {
  const notification = JSON.parse(event.data);
  showNotification(notification);
});

// Close connection
eventSource.close();

SSE with authentication

EventSource doesn't support custom headers. Use query params or cookies:

// Query param (less secure)
const eventSource = new EventSource(`/api/events?token=${token}`);

// Or use fetch with ReadableStream (more flexible)
async function streamEvents() {
  const response = await fetch('/api/events', {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });

  const reader = response.body?.getReader();
  const decoder = new TextDecoder();

  while (reader) {
    const { done, value } = await reader.read();
    if (done) break;
    
    const text = decoder.decode(value);
    const lines = text.split('\n');
    
    for (const line of lines) {
      if (line.startsWith('data: ')) {
        const data = JSON.parse(line.slice(6));
        handleEvent(data);
      }
    }
  }
}

React SSE hook

function useSSE<T>(url: string, onMessage: (data: T) => void) {
  useEffect(() => {
    const eventSource = new EventSource(url);
    
    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);
      onMessage(data);
    };
    
    eventSource.onerror = () => {
      // EventSource auto-reconnects
      console.log('SSE error, reconnecting...');
    };

    return () => {
      eventSource.close();
    };
  }, [url, onMessage]);
}

// Usage
function LiveFeed() {
  const [items, setItems] = useState<FeedItem[]>([]);
  
  useSSE<FeedItem>('/api/feed', (item) => {
    setItems((prev) => [item, ...prev].slice(0, 50));
  });

  return <FeedList items={items} />;
}

Connection state management

Exponential backoff

class ReconnectingWebSocket {
  private url: string;
  private socket: WebSocket | null = null;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 10;
  private baseDelay = 1000;

  constructor(url: string) {
    this.url = url;
    this.connect();
  }

  private connect() {
    this.socket = new WebSocket(this.url);
    
    this.socket.onopen = () => {
      this.reconnectAttempts = 0;
    };

    this.socket.onclose = () => {
      this.scheduleReconnect();
    };
  }

  private scheduleReconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error('Max reconnection attempts reached');
      return;
    }

    const delay = Math.min(
      this.baseDelay * Math.pow(2, this.reconnectAttempts),
      30000 // Max 30 seconds
    );
    
    this.reconnectAttempts++;
    
    setTimeout(() => this.connect(), delay);
  }
}

Message queue

Queue messages while disconnected:

class QueuedWebSocket {
  private socket: WebSocket | null = null;
  private messageQueue: string[] = [];

  send(data: any) {
    const message = JSON.stringify(data);
    
    if (this.socket?.readyState === WebSocket.OPEN) {
      this.socket.send(message);
    } else {
      this.messageQueue.push(message);
    }
  }

  private onConnect() {
    // Flush queue
    while (this.messageQueue.length > 0) {
      const message = this.messageQueue.shift()!;
      this.socket?.send(message);
    }
  }
}

Real-time patterns

Presence (who's online)

function usePresence(roomId: string) {
  const [users, setUsers] = useState<User[]>([]);
  const socket = useSocket();

  useEffect(() => {
    socket.emit('join', { roomId });
    
    socket.on('presence', (data) => {
      setUsers(data.users);
    });

    socket.on('user:join', (user) => {
      setUsers((prev) => [...prev, user]);
    });

    socket.on('user:leave', (userId) => {
      setUsers((prev) => prev.filter((u) => u.id !== userId));
    });

    return () => {
      socket.emit('leave', { roomId });
      socket.off('presence');
      socket.off('user:join');
      socket.off('user:leave');
    };
  }, [socket, roomId]);

  return users;
}

Typing indicators

function useTypingIndicator(roomId: string) {
  const [typingUsers, setTypingUsers] = useState<string[]>([]);
  const socket = useSocket();
  const typingTimeoutRef = useRef<NodeJS.Timeout>();

  useEffect(() => {
    socket.on('typing:start', ({ userId }) => {
      setTypingUsers((prev) => 
        prev.includes(userId) ? prev : [...prev, userId]
      );
    });

    socket.on('typing:stop', ({ userId }) => {
      setTypingUsers((prev) => prev.filter((id) => id !== userId));
    });

    return () => {
      socket.off('typing:start');
      socket.off('typing:stop');
    };
  }, [socket]);

  const startTyping = useCallback(() => {
    socket.emit('typing:start', { roomId });
    
    clearTimeout(typingTimeoutRef.current);
    typingTimeoutRef.current = setTimeout(() => {
      socket.emit('typing:stop', { roomId });
    }, 3000);
  }, [socket, roomId]);

  return { typingUsers, startTyping };
}

Optimistic updates with reconciliation

function useOptimisticMessages() {
  const [messages, setMessages] = useState<Message[]>([]);
  const pendingRef = useRef<Map<string, Message>>(new Map());
  const socket = useSocket();

  const sendMessage = (content: string) => {
    const tempId = crypto.randomUUID();
    const optimistic: Message = {
      id: tempId,
      content,
      status: 'pending',
      createdAt: new Date().toISOString(),
    };

    // Add optimistically
    setMessages((prev) => [...prev, optimistic]);
    pendingRef.current.set(tempId, optimistic);

    // Send to server
    socket.emit('message', { tempId, content });
  };

  useEffect(() => {
    // Server confirms message
    socket.on('message:confirmed', ({ tempId, message }) => {
      pendingRef.current.delete(tempId);
      setMessages((prev) =>
        prev.map((m) => (m.id === tempId ? message : m))
      );
    });

    // Server rejects message
    socket.on('message:failed', ({ tempId, error }) => {
      pendingRef.current.delete(tempId);
      setMessages((prev) =>
        prev.map((m) =>
          m.id === tempId ? { ...m, status: 'failed', error } : m
        )
      );
    });

    // New message from others
    socket.on('message:new', (message) => {
      setMessages((prev) => [...prev, message]);
    });
  }, [socket]);

  return { messages, sendMessage };
}

Debugging

Use the JSON Formatter to inspect WebSocket message payloads. Validate message schemas with JSON Validator. Generate unique message IDs with UUID Generator.

Browser DevTools

Network tab → WS filter shows WebSocket connections:

  • Messages sent and received
  • Connection timing
  • Close codes

Practical takeaway

Start with native WebSocket API for simple use cases. Use Socket.IO when you need rooms, reconnection, and fallbacks out of the box. Use SSE for one-way server-to-client streaming. Handle disconnection gracefully — show connection state to users, queue messages, implement reconnection with backoff. Keep messages small and schema-consistent. Test with network throttling and disconnection scenarios.

Sources

Try it on codefunc

Related articles

3

Search tools

Find a developer tool