20 KiB
20 KiB
📧 Email WebSocket Integration Guide for React
This guide will help you integrate real-time email notifications into your React application using WebSocket connections.
🚀 Quick Start
Installation
npm install socket.io-client
# or
yarn add socket.io-client
Basic Setup
import io from "socket.io-client";
const socket = io("ws://localhost:3000/email", {
auth: {
token: localStorage.getItem("jwt-token"), // Your JWT authentication token
},
});
🔌 Connection Setup
1. Basic Connection Hook
// hooks/useEmailWebSocket.ts
import { useEffect, useState } from "react";
import io, { Socket } from "socket.io-client";
interface UseEmailWebSocketOptions {
token: string;
serverUrl?: string;
onConnect?: () => void;
onDisconnect?: () => void;
onError?: (error: string) => void;
}
export const useEmailWebSocket = ({ token, serverUrl = "ws://localhost:3000/email", onConnect, onDisconnect, onError }: UseEmailWebSocketOptions) => {
const [socket, setSocket] = useState<Socket | null>(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
if (!token) return;
const newSocket = io(serverUrl, {
auth: { token },
transports: ["websocket", "polling"],
});
// Connection events
newSocket.on("connect", () => {
console.log("✅ Connected to email WebSocket");
setIsConnected(true);
onConnect?.();
});
newSocket.on("disconnect", () => {
console.log("❌ Disconnected from email WebSocket");
setIsConnected(false);
onDisconnect?.();
});
newSocket.on("connection_established", (data) => {
console.log("🎉 Email WebSocket connection established:", data);
});
newSocket.on("connection_error", (error) => {
console.error("💥 Email WebSocket connection error:", error);
onError?.(error.error);
});
// Health monitoring
newSocket.on("ping", () => {
newSocket.emit("pong", { timestamp: new Date().toISOString() });
});
setSocket(newSocket);
return () => {
newSocket.close();
setSocket(null);
};
}, [token, serverUrl]);
return { socket, isConnected };
};
2. Email Events Hook
// hooks/useEmailEvents.ts
import { useEffect, useState } from "react";
import { Socket } from "socket.io-client";
// TypeScript interfaces (copy from your backend)
export interface EmailPayload {
messageId: number;
userId: string;
from: {
name?: string;
address: string;
};
to: Array<{
name?: string;
address: string;
}>;
subject: string;
preview?: string;
hasAttachments: boolean;
timestamp: string;
mailboxId: string;
mailboxName: string;
isRead: boolean;
isFlagged: boolean;
size: number;
}
export interface EmailStatusPayload {
messageId: number;
userId: string;
mailboxId: string;
timestamp: string;
}
export interface UnreadCountPayload {
userId: string;
totalUnread: number;
mailboxCounts: Record<string, number>;
timestamp: string;
count: number;
}
interface EmailEventHandlers {
onNewEmail?: (email: EmailPayload) => void;
onEmailRead?: (data: EmailStatusPayload) => void;
onEmailDeleted?: (data: EmailStatusPayload) => void;
onEmailSent?: (data: any) => void;
onUnreadCountUpdated?: (data: UnreadCountPayload) => void;
onMailboxUpdated?: (data: any) => void;
}
export const useEmailEvents = (socket: Socket | null, handlers: EmailEventHandlers) => {
useEffect(() => {
if (!socket) return;
// Register event handlers
if (handlers.onNewEmail) {
socket.on("new_email", handlers.onNewEmail);
}
if (handlers.onEmailRead) {
socket.on("email_read", handlers.onEmailRead);
}
if (handlers.onEmailDeleted) {
socket.on("email_deleted", handlers.onEmailDeleted);
}
if (handlers.onEmailSent) {
socket.on("email_sent", handlers.onEmailSent);
}
if (handlers.onUnreadCountUpdated) {
socket.on("unread_count_updated", handlers.onUnreadCountUpdated);
}
if (handlers.onMailboxUpdated) {
socket.on("mailbox_updated", handlers.onMailboxUpdated);
}
// Cleanup on unmount
return () => {
socket.off("new_email");
socket.off("email_read");
socket.off("email_deleted");
socket.off("email_sent");
socket.off("unread_count_updated");
socket.off("mailbox_updated");
};
}, [socket, handlers]);
};
📱 Complete React Integration
1. Email Notification Component
// components/EmailNotifications.tsx
import React, { useState, useCallback } from 'react';
import { useEmailWebSocket, useEmailEvents, EmailPayload, UnreadCountPayload } from '../hooks';
interface EmailNotificationsProps {
userToken: string;
onEmailClick?: (email: EmailPayload) => void;
}
export const EmailNotifications: React.FC<EmailNotificationsProps> = ({
userToken,
onEmailClick
}) => {
const [notifications, setNotifications] = useState<EmailPayload[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
const [isVisible, setIsVisible] = useState(false);
const { socket, isConnected } = useEmailWebSocket({
token: userToken,
onConnect: () => console.log('Email notifications connected'),
onError: (error) => console.error('WebSocket error:', error),
});
const handleNewEmail = useCallback((email: EmailPayload) => {
console.log('📧 New email received:', email);
// Add to notifications list
setNotifications(prev => [email, ...prev.slice(0, 9)]); // Keep only 10 latest
// Show browser notification if permission granted
if (Notification.permission === 'granted') {
new Notification(`📧 New Email: ${email.subject}`, {
body: `From: ${email.from.address}\n${email.preview}`,
icon: '/email-icon.png',
tag: `email-${email.messageId}`, // Prevent duplicates
});
}
// Play notification sound (optional)
const audio = new Audio('/notification.mp3');
audio.play().catch(() => {}); // Ignore if audio fails
}, []);
const handleUnreadCountUpdate = useCallback((data: UnreadCountPayload) => {
setUnreadCount(data.count);
}, []);
const handleEmailRead = useCallback((data: any) => {
// Remove from notifications if it was there
setNotifications(prev =>
prev.filter(notification => notification.messageId !== data.messageId)
);
}, []);
useEmailEvents(socket, {
onNewEmail: handleNewEmail,
onUnreadCountUpdated: handleUnreadCountUpdate,
onEmailRead: handleEmailRead,
});
// Request notification permission on mount
React.useEffect(() => {
if (Notification.permission === 'default') {
Notification.requestPermission();
}
}, []);
return (
<div className="email-notifications">
{/* Connection Status */}
<div className={`connection-status ${isConnected ? 'connected' : 'disconnected'}`}>
<span className="status-dot"></span>
{isConnected ? 'Connected' : 'Disconnected'}
</div>
{/* Notification Bell */}
<div
className="notification-bell"
onClick={() => setIsVisible(!isVisible)}
>
🔔
{unreadCount > 0 && (
<span className="badge">{unreadCount > 99 ? '99+' : unreadCount}</span>
)}
</div>
{/* Notifications Dropdown */}
{isVisible && (
<div className="notifications-dropdown">
<div className="notifications-header">
<h3>📧 Email Notifications</h3>
<span>{unreadCount} unread</span>
</div>
<div className="notifications-list">
{notifications.length === 0 ? (
<div className="no-notifications">No new emails</div>
) : (
notifications.map((email) => (
<div
key={email.messageId}
className="notification-item"
onClick={() => onEmailClick?.(email)}
>
<div className="sender">
<strong>{email.from.name || email.from.address}</strong>
{email.hasAttachments && <span className="attachment-icon">📎</span>}
</div>
<div className="subject">{email.subject}</div>
<div className="preview">{email.preview}</div>
<div className="timestamp">
{new Date(email.timestamp).toLocaleString()}
</div>
</div>
))
)}
</div>
</div>
)}
</div>
);
};
2. Email Dashboard Component
// components/EmailDashboard.tsx
import React, { useState } from 'react';
import { useEmailWebSocket, useEmailEvents } from '../hooks';
interface EmailDashboardProps {
userToken: string;
}
export const EmailDashboard: React.FC<EmailDashboardProps> = ({ userToken }) => {
const [emailStats, setEmailStats] = useState({
totalUnread: 0,
newToday: 0,
sentToday: 0,
});
const { socket, isConnected } = useEmailWebSocket({
token: userToken,
});
useEmailEvents(socket, {
onNewEmail: (email) => {
setEmailStats(prev => ({
...prev,
totalUnread: prev.totalUnread + 1,
newToday: prev.newToday + 1,
}));
},
onEmailSent: () => {
setEmailStats(prev => ({
...prev,
sentToday: prev.sentToday + 1,
}));
},
onUnreadCountUpdated: (data) => {
setEmailStats(prev => ({
...prev,
totalUnread: data.count,
}));
},
});
return (
<div className="email-dashboard">
<h2>📧 Email Dashboard</h2>
<div className="stats-grid">
<div className="stat-card">
<h3>{emailStats.totalUnread}</h3>
<p>Unread Emails</p>
</div>
<div className="stat-card">
<h3>{emailStats.newToday}</h3>
<p>Received Today</p>
</div>
<div className="stat-card">
<h3>{emailStats.sentToday}</h3>
<p>Sent Today</p>
</div>
<div className="stat-card">
<h3 className={isConnected ? 'connected' : 'disconnected'}>
{isConnected ? '🟢' : '🔴'}
</h3>
<p>{isConnected ? 'Connected' : 'Disconnected'}</p>
</div>
</div>
</div>
);
};
3. Context Provider (Advanced)
// context/EmailWebSocketContext.tsx
import React, { createContext, useContext, ReactNode } from 'react';
import { useEmailWebSocket } from '../hooks/useEmailWebSocket';
import { Socket } from 'socket.io-client';
interface EmailWebSocketContextValue {
socket: Socket | null;
isConnected: boolean;
}
const EmailWebSocketContext = createContext<EmailWebSocketContextValue | null>(null);
interface EmailWebSocketProviderProps {
children: ReactNode;
token: string;
serverUrl?: string;
}
export const EmailWebSocketProvider: React.FC<EmailWebSocketProviderProps> = ({
children,
token,
serverUrl,
}) => {
const { socket, isConnected } = useEmailWebSocket({ token, serverUrl });
return (
<EmailWebSocketContext.Provider value={{ socket, isConnected }}>
{children}
</EmailWebSocketContext.Provider>
);
};
export const useEmailWebSocketContext = () => {
const context = useContext(EmailWebSocketContext);
if (!context) {
throw new Error('useEmailWebSocketContext must be used within EmailWebSocketProvider');
}
return context;
};
🎨 CSS Styles
/* styles/email-notifications.css */
.email-notifications {
position: relative;
display: inline-block;
}
.connection-status {
position: absolute;
top: -8px;
right: -8px;
font-size: 10px;
padding: 2px 6px;
border-radius: 10px;
display: flex;
align-items: center;
gap: 4px;
}
.connection-status.connected {
background: #d4edda;
color: #155724;
}
.connection-status.disconnected {
background: #f8d7da;
color: #721c24;
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
.notification-bell {
position: relative;
cursor: pointer;
font-size: 24px;
padding: 8px;
border-radius: 50%;
transition: background-color 0.2s;
}
.notification-bell:hover {
background-color: #f5f5f5;
}
.badge {
position: absolute;
top: 0;
right: 0;
background: #dc3545;
color: white;
border-radius: 50%;
min-width: 18px;
height: 18px;
font-size: 11px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
.notifications-dropdown {
position: absolute;
top: 100%;
right: 0;
width: 350px;
max-height: 400px;
background: white;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1000;
overflow: hidden;
}
.notifications-header {
padding: 12px 16px;
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
display: flex;
justify-content: space-between;
align-items: center;
}
.notifications-list {
max-height: 300px;
overflow-y: auto;
}
.notification-item {
padding: 12px 16px;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background-color 0.2s;
}
.notification-item:hover {
background-color: #f8f9fa;
}
.notification-item:last-child {
border-bottom: none;
}
.sender {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.attachment-icon {
font-size: 12px;
}
.subject {
font-weight: 500;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.preview {
color: #666;
font-size: 14px;
margin-bottom: 4px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.timestamp {
font-size: 12px;
color: #999;
}
.no-notifications {
padding: 20px;
text-align: center;
color: #666;
}
/* Dashboard Styles */
.email-dashboard {
padding: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-top: 20px;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 8px;
border: 1px solid #ddd;
text-align: center;
}
.stat-card h3 {
margin: 0 0 8px 0;
font-size: 32px;
font-weight: bold;
}
.stat-card h3.connected {
color: #28a745;
}
.stat-card h3.disconnected {
color: #dc3545;
}
.stat-card p {
margin: 0;
color: #666;
font-size: 14px;
}
🧪 Testing & Manual Triggers
Manual Email Check
// API call to manually trigger email check
const triggerEmailCheck = async () => {
try {
const response = await fetch("/api/email/gateway/check-emails", {
method: "POST",
headers: {
Authorization: `Bearer ${userToken}`,
"Content-Type": "application/json",
},
});
const result = await response.json();
console.log("✅ Manual email check triggered:", result);
} catch (error) {
console.error("❌ Failed to trigger email check:", error);
}
};
Send Test Notification
const sendTestNotification = async () => {
try {
const response = await fetch("/api/email/gateway/test-notification", {
method: "POST",
headers: {
Authorization: `Bearer ${userToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
message: "Test notification from React app!",
}),
});
const result = await response.json();
console.log("✅ Test notification sent:", result);
} catch (error) {
console.error("❌ Failed to send test notification:", error);
}
};
🔧 Complete App Integration
// App.tsx - Complete example
import React from 'react';
import { EmailWebSocketProvider } from './context/EmailWebSocketContext';
import { EmailNotifications } from './components/EmailNotifications';
import { EmailDashboard } from './components/EmailDashboard';
function App() {
const userToken = localStorage.getItem('jwt-token') || '';
const handleEmailClick = (email: any) => {
console.log('Email clicked:', email);
// Navigate to email detail page
// router.push(`/emails/${email.messageId}`);
};
if (!userToken) {
return <div>Please login to access email notifications</div>;
}
return (
<EmailWebSocketProvider token={userToken}>
<div className="app">
<header>
<h1>📧 Email App</h1>
<EmailNotifications
userToken={userToken}
onEmailClick={handleEmailClick}
/>
</header>
<main>
<EmailDashboard userToken={userToken} />
</main>
</div>
</EmailWebSocketProvider>
);
}
export default App;
🚨 Error Handling & Edge Cases
Connection Recovery
const useConnectionRecovery = (socket: Socket | null) => {
useEffect(() => {
if (!socket) return;
const handleReconnect = () => {
console.log("🔄 WebSocket reconnecting...");
// Optionally refresh data or show reconnecting state
};
const handleReconnectError = () => {
console.error("❌ Failed to reconnect");
// Show user notification or retry logic
};
socket.on("reconnect", handleReconnect);
socket.on("reconnect_error", handleReconnectError);
return () => {
socket.off("reconnect", handleReconnect);
socket.off("reconnect_error", handleReconnectError);
};
}, [socket]);
};
Token Refresh
const useTokenRefresh = (socket: Socket | null) => {
useEffect(() => {
const refreshToken = () => {
const newToken = localStorage.getItem("jwt-token");
if (socket && newToken) {
socket.auth = { token: newToken };
socket.disconnect();
socket.connect();
}
};
// Listen for token refresh events
window.addEventListener("token-refreshed", refreshToken);
return () => {
window.removeEventListener("token-refreshed", refreshToken);
};
}, [socket]);
};
📝 Environment Configuration
// config/websocket.ts
export const WEBSOCKET_CONFIG = {
development: {
serverUrl: "ws://localhost:3000/email",
reconnectionAttempts: 5,
timeout: 20000,
},
production: {
serverUrl: "wss://your-domain.com/email",
reconnectionAttempts: 10,
timeout: 30000,
},
};
export const getWebSocketConfig = () => {
const env = process.env.NODE_ENV || "development";
return WEBSOCKET_CONFIG[env as keyof typeof WEBSOCKET_CONFIG];
};
🎯 Best Practices
- 🔐 Security: Always use HTTPS/WSS in production
- 🔄 Reconnection: Implement proper reconnection logic
- 🎯 Performance: Limit notification history (max 10-50 items)
- 📱 UX: Request notification permissions gracefully
- 🔊 Audio: Make notification sounds optional
- 💾 Persistence: Consider storing notifications in localStorage
- 🎨 Theming: Support light/dark mode for notifications
- ♿ Accessibility: Add proper ARIA labels and keyboard navigation
🐛 Troubleshooting
Connection Issues:
- Check JWT token validity
- Verify server URL and port
- Check browser console for errors
- Ensure CORS is properly configured
Missing Notifications:
- Verify WebSocket event names match backend
- Check browser notification permissions
- Confirm user is authenticated properly
- Test with manual triggers
Performance Issues:
- Limit notification history
- Debounce rapid events
- Use React.memo for components
- Implement virtual scrolling for long lists
🚀 Ready to Go!
Your React app is now ready to receive real-time email notifications! The WebSocket connection will automatically:
- ✅ Authenticate users with JWT tokens
- ✅ Receive new email notifications instantly
- ✅ Update unread counts in real-time
- ✅ Handle connection recovery automatically
- ✅ Show browser notifications (with permission)
- ✅ Provide a beautiful UI for email notifications
Happy coding! 🎉