DEV Community

Sambalicious
Sambalicious

Posted on

Scalable Notifications in React with Pub/Sub (No State Store)

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

  1. 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.

  2. 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.

  3. Asynchronous Communication: Publishers fire events and move on—no waiting for responses. They can send messages and continue their work without blocking.

  4. 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);
  });
};
Enter fullscreen mode Exit fullscreen mode

(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]);
};
Enter fullscreen mode Exit fullscreen mode

3. Usage Example: Listening for a Specific Notification

useNotificationEvent("OrderDelivered", (notification) => {
  // Do whatever with the notification, e.g., show a toast or modal
});
Enter fullscreen mode Exit fullscreen mode

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
    });
}, []);
Enter fullscreen mode Exit fullscreen mode

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)