Real-Time WebSocket Notifications Implementation on Website
WebSocket notifications deliver events to user instantly without polling: new message, order status, mention in comment, another user's action.
Notification Service Architecture
// notification-ws.service.ts
class NotificationWebSocketService {
private userSockets = new Map<string, Set<string>>(); // userId → socketIds
async onConnect(socket: Socket, userId: string) {
// One user can have multiple tabs/devices
if (!this.userSockets.has(userId)) {
this.userSockets.set(userId, new Set());
}
this.userSockets.get(userId)!.add(socket.id);
socket.join(`user:${userId}`);
// Deliver undelivered notifications
const pending = await this.notificationRepo.findUndelivered(userId);
if (pending.length > 0) {
socket.emit('notifications:batch', pending);
await this.notificationRepo.markDelivered(pending.map(n => n.id));
}
}
async sendToUser(userId: string, notification: Notification): Promise<void> {
const isOnline = this.userSockets.has(userId) &&
this.userSockets.get(userId)!.size > 0;
if (isOnline) {
// User online — deliver immediately
io.to(`user:${userId}`).emit('notification:new', notification);
await this.notificationRepo.markDelivered([notification.id]);
} else {
// Offline — save for delivery on connect
await this.notificationRepo.save({ ...notification, status: 'pending' });
// Can send push notification or email
await this.pushService.send(userId, notification);
}
}
}
Notification Types
type NotificationType =
| 'order:status_changed'
| 'message:received'
| 'mention:comment'
| 'task:assigned'
| 'payment:processed'
| 'system:alert';
interface Notification {
id: string;
type: NotificationType;
title: string;
body: string;
actionUrl?: string;
data?: Record<string, unknown>;
createdAt: Date;
readAt?: Date;
}
React: Handling Notifications
// hooks/useNotifications.ts
function useNotifications() {
const [notifications, setNotifications] = useState<Notification[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
const socket = useSocket();
useEffect(() => {
if (!socket) return;
socket.on('notification:new', (notification: Notification) => {
setNotifications(prev => [notification, ...prev]);
setUnreadCount(prev => prev + 1);
showToast(notification);
});
socket.on('notifications:batch', (batch: Notification[]) => {
setNotifications(prev => [...batch, ...prev]);
setUnreadCount(prev => prev + batch.filter(n => !n.readAt).length);
});
return () => {
socket.off('notification:new');
socket.off('notifications:batch');
};
}, [socket]);
const markAsRead = async (id: string) => {
await fetch(`/api/notifications/${id}/read`, { method: 'POST' });
setNotifications(prev =>
prev.map(n => n.id === id ? { ...n, readAt: new Date() } : n)
);
setUnreadCount(prev => Math.max(0, prev - 1));
};
const markAllAsRead = async () => {
await fetch('/api/notifications/read-all', { method: 'POST' });
setNotifications(prev => prev.map(n => ({ ...n, readAt: n.readAt || new Date() })));
setUnreadCount(0);
};
return { notifications, unreadCount, markAsRead, markAllAsRead };
}
Toast Notifications
function showToast(notification: Notification) {
const { type, title, body, actionUrl } = notification;
const icons: Record<NotificationType, string> = {
'order:status_changed': '📦',
'message:received': '💬',
'mention:comment': '@',
'task:assigned': '✅',
'payment:processed': '💳',
'system:alert': '⚠️'
};
toast.custom(() => (
<div className="notification-toast" onClick={() => actionUrl && navigate(actionUrl)}>
<span className="icon">{icons[type]}</span>
<div>
<p className="title">{title}</p>
<p className="body">{body}</p>
</div>
</div>
), { duration: 5000 });
}
Persistent Bell
function NotificationBell() {
const { notifications, unreadCount, markAsRead, markAllAsRead } = useNotifications();
const [isOpen, setIsOpen] = useState(false);
return (
<div className="relative">
<button onClick={() => setIsOpen(!isOpen)} className="relative">
🔔
{unreadCount > 0 && (
<span className="badge">{unreadCount > 99 ? '99+' : unreadCount}</span>
)}
</button>
{isOpen && (
<div className="notification-dropdown">
<div className="header">
<h3>Notifications</h3>
<button onClick={markAllAsRead}>Mark all read</button>
</div>
{notifications.slice(0, 20).map(n => (
<NotificationItem key={n.id} notification={n} onRead={markAsRead} />
))}
</div>
)}
</div>
);
}
Timeline
WebSocket notifications with offline delivery + React hook + bell component: 1–2 weeks.







