Recently, I needed to add a flexible, scalable notification system to a large-scale React/Next.js application. The backend provided a handy endpoint that returns all pending notifications at once. My initial thought was simple: fetch them globally and render a single Notification component that switches UI based on the notification type.
Quickly, two problems emerged:
- The endpoint could return multiple notifications at once—how do I handle queuing and displaying them without chaos?
- For custom notification types (e.g., interactive modals, special banners), the single component would become bloated with conditionals and logic.
After some research, I landed on a classic design pattern perfectly suited for this: Publisher/Subscriber (Pub/Sub).
What is Pub/Sub?
Pub/Sub is an asynchronous messaging pattern that decouples the message sender (Publisher) from the receivers (Subscribers). Publishers broadcast events to a central event bus (or broker) without knowing who (if anyone) is listening. Subscribers register interest in specific event types and react only when relevant messages arrive.
It’s like a radio station: the broadcaster sends signals blindly, and anyone tuned to the right frequency picks them up.
The publisher sends messages to the topic without knowing who will receive them through a broker (Event bus) to subscribers, who receive only relevant messages that they subscribed to. This pattern allows for independent, asynchronous component operation.
Core Principles & Benefits
Loose Coupling: Publishers and subscribers don’t know about each other—no direct imports or references. They are not directly connected and do not need to know each other's identities or locations.
Scalability: Easily add more subscribers to a topic as your app grows; the bus handles distribution. The system can handle a large volume of events by adding more subscribers without impacting performance.
Asynchronous Communication: Publishers fire events and move on—no waiting for responses. They can send messages and continue their work without blocking.
Many-to-Many Relationships: One publisher can reach many subscribers, and one subscriber can listen to many publishers.
When to Use It in React/Next.js
Pub/Sub is ideal for fire-and-forget events like notifications, analytics tracking, theme changes, or global modals. Avoid it for complex shared state—use Zustand, Redux, Jotai, or Context instead.
Implementation: Lightweight Notification Event Bus
Let’s build it step by step.
1. The Event Bus
type EventKey = "Login" | "Registration" | "PasswordReset"; // Extend with your notification types
const listeners = new Map<string, Set<(...args: unknown[]) => void>>();
export const notificationEventBus = {
subscribe: (eventKey: EventKey, callback: (...args: unknown[]) => void) => {
if (!listeners.has(eventKey)) {
listeners.set(eventKey, new Set());
}
listeners.get(eventKey)?.add(callback);
// Return unsubscribe function
return () => {
listeners.get(eventKey)?.delete(callback);
};
},
publish: (eventKey: EventKey, payload: unknown) => {
const callbacks = listeners.get(eventKey);
if (!callbacks) return;
callbacks.forEach((cb) => {
try {
cb(payload);
} catch (error) {
console.error(
`Error in notificationEventBus subscriber for event "${String(eventKey)}". Check the implementation of this subscriber to handle the error appropriately.`,
error,
);
}
});
},
};
// Helper function to publish all notifications at once
export const publishNotifications = (
notifications: NotificationData<PopupNotificationDetails>[],
) => {
notifications.forEach((notification) => {
notificationEventBus.publish(notification.type, notification);
});
};
(Note: NotificationData and PopupNotificationDetails are placeholders—define them based on your notification structure, e.g., { type: EventKey; message: string; }.)
2. React Hook for Safe Subscriptions
"use client";
import { useEffect, useRef } from "react";
import type { EventKey } from "@soar/types"; // Adjust import based on your types
import { notificationEventBus } from "../helpers";
export const useNotificationEvent = <T = unknown>(
eventKey: EventKey,
handler: (data: T) => void,
) => {
const handlerRef = useRef(handler);
// Keep handler up-to-date without re-subscribing
useEffect(() => {
handlerRef.current = handler;
});
useEffect(() => {
const stableHandler = (data: unknown) => {
handlerRef.current(data as T);
};
const unsubscribe = notificationEventBus.subscribe(
eventKey,
stableHandler as (...args: unknown[]) => void,
);
return () => {
unsubscribe();
};
}, [eventKey]);
};
3. Usage Example: Listening for a Specific Notification
useNotificationEvent("OrderDelivered", (notification) => {
// Do whatever with the notification, e.g., show a toast or modal
});
4. Fetch and Publish Notifications in Your Component
// Example: Fetch and publish
// For one-time fetch; use polling or WebSockets for real-time.
useEffect(() => {
fetch("/api/notifications")
.then((res) => res.json())
.then((notifications) => {
publishNotifications(notifications); // Your helper function
});
}, []);
Real-World Tips
- Multiple Notifications: Publish per type—each subscriber handles its own UI cleanly.
- Custom Notifications: Create dedicated subscriber components instead of one giant switch statement.
Conclusion
Pub/Sub gave me exactly what I needed: decoupled, scalable notifications without bloating components or pulling in heavy state management. It’s simple, performant, and easy to reason about.
Try it in your Next.js/React app—start with this event bus and hook, then expand to toasts, modals, or analytics. I made a little POC for this; fork the code and tweak it! Let me know how it works for you. 🚀
Questions or improvements? Hit me up on X: @sambalicious_
Top comments (0)