WebSocket Real-Time Messaging Guide
WebSockets enable true bidirectional communication between client and server, essential for real-time chat where messages must arrive instantly without polling. Unlike REST APIs where the client pulls data, WebSockets let the server push messages to all connected clients the moment a user sends them. This guide covers integrating a WebSocket client into your React chat app, managing connection lifecycle, and gracefully handling disconnections.
The core challenge is decoupling the WebSocket client (which lives outside React's render cycle) from your component state (which updates via Context or a state management library). We'll create a separate WebSocket service module that emits events, and let your chat reducer subscribe to those events and update state. This prevents race conditions and makes testing easier.
WebSocket Client Module
Create a standalone WebSocket client that is framework-agnostic:
// wsClient.js
class ChatWebSocketClient {
constructor(url, handlers) {
this.url = url;
this.handlers = handlers; // { onMessage, onTyping, onPresence, onError, onOpen }
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000;
this.messageQueue = [];
}
connect() {
return new Promise((resolve, reject) => {
try {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.reconnectAttempts = 0;
this.reconnectDelay = 1000;
this.flushQueue();
if (this.handlers.onOpen) {
this.handlers.onOpen();
}
resolve();
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.handleMessage(data);
} catch (err) {
console.error('Failed to parse WebSocket message:', err);
}
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
if (this.handlers.onError) {
this.handlers.onError(error);
}
reject(error);
};
this.ws.onclose = () => {
console.log('WebSocket closed');
this.attemptReconnect();
};
} catch (err) {
reject(err);
}
});
}
handleMessage(data) {
const { type, payload } = data;
switch (type) {
case 'message':
if (this.handlers.onMessage) {
this.handlers.onMessage(payload);
}
break;
case 'typing':
if (this.handlers.onTyping) {
this.handlers.onTyping(payload);
}
break;
case 'presence':
if (this.handlers.onPresence) {
this.handlers.onPresence(payload);
}
break;
case 'ack':
// Server acknowledges message receipt
if (this.handlers.onAck) {
this.handlers.onAck(payload);
}
break;
default:
console.warn('Unknown message type:', type);
}
}
send(message) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
} else {
console.warn('WebSocket not connected, queueing message');
this.messageQueue.push(message);
}
}
flushQueue() {
while (this.messageQueue.length > 0) {
const msg = this.messageQueue.shift();
this.send(msg);
}
}
attemptReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Max reconnection attempts reached');
if (this.handlers.onError) {
this.handlers.onError(new Error('Failed to reconnect'));
}
return;
}
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
console.log(`Attempting to reconnect in ${delay}ms...`);
setTimeout(() => {
this.connect().catch((err) => {
console.error('Reconnection failed:', err);
});
}, delay);
}
close() {
if (this.ws) {
this.ws.close();
}
}
isConnected() {
return this.ws && this.ws.readyState === WebSocket.OPEN;
}
}
export const createChatWebSocketClient = (url, handlers) => {
return new ChatWebSocketClient(url, handlers);
};
Integrating WebSocket into ChatProvider
Your ChatProvider initializes the WebSocket client and dispatches updates:
function ChatProvider({ children, wsUrl = 'wss://api.chat.example.com/socket' }) {
const [state, dispatch] = useReducer(chatReducer, initialState);
const wsClientRef = useRef(null);
useEffect(() => {
const wsClient = createChatWebSocketClient(wsUrl, {
onOpen: () => {
dispatch({ type: 'SET_CONNECTION_STATUS', payload: 'connected' });
},
onMessage: (payload) => {
// Payload: { channelId, userId, messageId, text, timestamp }
dispatch({ type: 'ADD_MESSAGE', payload });
},
onTyping: (payload) => {
// Payload: { channelId, userId, isTyping }
dispatch({ type: 'SET_TYPING', payload });
},
onPresence: (payload) => {
// Payload: { channelId, userId, status, lastSeen }
dispatch({ type: 'UPDATE_PRESENCE', payload });
},
onAck: (payload) => {
// Payload: { messageId, channelId, timestamp }
dispatch({ type: 'ACK_MESSAGE', payload });
},
onError: () => {
dispatch({ type: 'SET_CONNECTION_STATUS', payload: 'disconnected' });
},
});
wsClientRef.current = wsClient;
// Connect on mount
wsClient.connect().catch((err) => {
console.error('Initial WebSocket connection failed:', err);
dispatch({ type: 'SET_CONNECTION_STATUS', payload: 'failed' });
});
return () => {
wsClient.close();
};
}, [wsUrl]);
const sendMessage = (channelId, text) => {
const message = {
id: `msg_${Date.now()}_${Math.random()}`,
channelId,
userId: state.currentUser.id,
text,
timestamp: Date.now(),
};
// Optimistically update local state
dispatch({ type: 'ADD_MESSAGE', payload: message });
// Send to server
wsClientRef.current?.send({
type: 'message',
payload: message,
});
};
return (
<ChatContext.Provider value={{ state, dispatch, sendMessage }}>
{children}
</ChatContext.Provider>
);
}
Handling Connection Status
Show a status indicator so users know if messages are live or queued:
function ConnectionStatus() {
const { state } = useContext(ChatContext);
const statusColor = {
connected: '#28a745',
disconnected: '#ffc107',
reconnecting: '#fd7e14',
failed: '#dc3545',
};
const statusLabel = {
connected: 'Connected',
disconnected: 'Offline (reconnecting...)',
reconnecting: 'Reconnecting...',
failed: 'Connection failed',
};
return (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
padding: '4px 8px',
fontSize: '12px',
color: '#fff',
backgroundColor: statusColor[state.connectionStatus],
borderRadius: '4px',
}}
>
<span
style={{
display: 'inline-block',
width: '8px',
height: '8px',
backgroundColor: '#fff',
borderRadius: '50%',
}}
/>
{statusLabel[state.connectionStatus]}
</div>
);
}
Message Acknowledgment and Error Handling
When the server receives a message, it sends back an ACK. Use this to confirm delivery:
// In your reducer, handle ACK to mark messages as sent
function chatReducer(state, action) {
switch (action.type) {
case 'ADD_MESSAGE': {
const { payload } = action;
const channel = state.channels[payload.channelId];
return {
...state,
channels: {
...state.channels,
[payload.channelId]: {
...channel,
messages: [...channel.messages, { ...payload, sent: false }],
},
},
offlineQueue: [...state.offlineQueue, payload], // Add to offline queue
};
}
case 'ACK_MESSAGE': {
const { payload } = action;
const channel = state.channels[payload.channelId];
const messageIndex = channel.messages.findIndex(
m => m.id === payload.messageId
);
if (messageIndex !== -1) {
const updatedMessages = [...channel.messages];
updatedMessages[messageIndex] = {
...updatedMessages[messageIndex],
sent: true,
timestamp: payload.timestamp,
};
return {
...state,
channels: {
...state.channels,
[payload.channelId]: {
...channel,
messages: updatedMessages,
},
},
offlineQueue: state.offlineQueue.filter(m => m.id !== payload.messageId),
};
}
return state;
}
default:
return state;
}
}
Key Takeaways
- WebSocket client is stateless: It emits events; your reducer handles state updates.
- Queue messages while disconnected: Store them locally and replay when reconnected.
- Exponential backoff for reconnection: Avoid overwhelming the server with repeated connection attempts.
- Separate UI status from connection state: Show a banner when disconnected so users understand why their messages feel delayed.
Frequently Asked Questions
What's the difference between WebSockets and Socket.IO?
WebSocket is a low-level browser API; Socket.IO is a library that wraps it with fallbacks (HTTP long-polling) for older browsers. For modern apps targeting 2026 browsers, plain WebSocket is sufficient and lighter. Use Socket.IO if you need IE11 support or want automatic reconnection helpers.
How do I handle server-sent error messages?
Add an error message type to your WebSocket protocol: { type: 'error', code: 'UNAUTHORIZED', message: 'You lack permission to send here' }. Dispatch these to state and show a toast notification or modal. Never silently fail.
Should I resend messages if the ACK times out?
Implement a message timeout: if no ACK arrives within 5 seconds, show a warning in the UI (don't auto-retry, as the message may have been processed despite the ACK being lost). Let the user manually resend.
How do I secure my WebSocket connection?
Use wss:// (WebSocket Secure) instead of ws://, just like HTTPS. On connection, send an auth token in the first message or as a query parameter, and have the server validate it before accepting messages from that client.