Publisher Subscriber Pattern in JavaScript
January 4, 2026
Publisher Subscriber Pattern in JavaScript
The Publisher/Subscriber (PubSub) pattern is a messaging pattern where senders of messages (publishers) don't program the messages to be sent directly to specific receivers (subscribers). Instead, publishers and subscribers are decoupled through a message broker or event channel.
What is the PubSub Pattern?
The PubSub pattern allows modules to communicate with each other without depending directly on each other. It's a form of loose coupling that promotes better code organization and maintainability.
Key Concepts
- Publisher: The component that publishes events/messages
- Subscriber: The component that listens for and reacts to events
- Topic/Channel: The medium through which publishers and subscribers communicate
- Decoupling: Publishers don't know about subscribers, and vice versa
Basic Implementation
Here's a simple PubSub implementation:
class PubSub {
constructor() {
this.topics = {};
}
subscribe(topic, callback) {
// Create the topic if it doesn't exist
if (!this.topics[topic]) {
this.topics[topic] = [];
}
// Add the callback to the topic's subscribers
this.topics[topic].push(callback);
// Return a reference to the subscription (useful for unsubscribing)
return {
topic,
callback,
};
}
// Publish to a topic
publish(topic, data) {
if (!this.topics[topic] || !this.topics[topic].length) {
return;
}
// Notify all subscribers with the provided data
this.topics[topic].forEach((callback) => {
callback(data);
});
}
// Unsubscribe from a topic
unsubscribe(subscription) {
const { topic, callback } = subscription;
// If the topic exists and has subscribers, remove the specified callback
if (this.topics[topic]) {
this.topics[topic] = this.topics[topic].filter((val) => val !== callback);
// If no more subscribers for the topic, remove the topic
if (!this.topics[topic].length) {
delete this.topics[topic];
}
}
}
}
// Usage
const pubSub = new PubSub();
// Subscribe to a topic
const subscription = pubSub.subscribe("news", (data) => {
console.log("Received news:", data);
});
// Publish to the 'news' topic
pubSub.publish("news", "Breaking news: JavaScript is awesome!");
// Unsubscribe from the topic
pubSub.unsubscribe(subscription);
How It Works
- Subscribers register for topics they're interested in
- Publishers send messages to topics without knowing who's listening
- The PubSub system routes messages to all subscribers of that topic
- Subscribers can unsubscribe when they no longer need updates
Real-World Examples
Example 1: News System
const pubSub = new PubSub();
// Multiple subscribers for the same topic
const subscriber1 = pubSub.subscribe("news", (article) => {
console.log("Subscriber 1 received:", article.title);
});
const subscriber2 = pubSub.subscribe("news", (article) => {
console.log("Subscriber 2 received:", article.title);
});
// Publisher sends news
pubSub.publish("news", {
title: "New JavaScript Features",
content: "ES2024 introduces new features...",
});
// Output:
// Subscriber 1 received: New JavaScript Features
// Subscriber 2 received: New JavaScript Features
Example 2: E-commerce System
const pubSub = new PubSub();
// Cart service subscribes to product updates
pubSub.subscribe("product:added", (product) => {
console.log(`Product ${product.name} added to cart`);
updateCartUI(product);
});
// Inventory service subscribes to the same event
pubSub.subscribe("product:added", (product) => {
decreaseInventory(product.id);
});
// Checkout service subscribes to purchase events
pubSub.subscribe("purchase:completed", (order) => {
processPayment(order);
sendConfirmationEmail(order);
});
// When user adds product
pubSub.publish("product:added", {
id: 123,
name: "Laptop",
price: 999
});
// When purchase completes
pubSub.publish("purchase:completed", {
orderId: "ORD-123",
items: [...],
total: 1998
});
Example 3: UI Component Communication
const pubSub = new PubSub();
// Header component subscribes to search events
pubSub.subscribe("search:performed", (query) => {
updateSearchHistory(query);
});
// Sidebar subscribes to filter changes
pubSub.subscribe("filter:changed", (filters) => {
updateSidebarFilters(filters);
});
// Main content area subscribes to both
pubSub.subscribe("search:performed", (query) => {
performSearch(query);
});
pubSub.subscribe("filter:changed", (filters) => {
applyFilters(filters);
});
// User performs search
pubSub.publish("search:performed", "JavaScript");
// User changes filters
pubSub.publish("filter:changed", { category: "tech", price: "under-100" });
Advanced Features
1. Once Subscription
Subscribe to an event only once:
class PubSub {
// ... existing code ...
subscribeOnce(topic, callback) {
const subscription = this.subscribe(topic, (data) => {
callback(data);
this.unsubscribe(subscription);
});
return subscription;
}
}
// Usage
pubSub.subscribeOnce("user:login", (user) => {
console.log("User logged in:", user.name);
// Automatically unsubscribes after first event
});
2. Priority Subscribers
Execute subscribers in a specific order:
class PubSub {
constructor() {
this.topics = {};
}
subscribe(topic, callback, priority = 0) {
if (!this.topics[topic]) {
this.topics[topic] = [];
}
this.topics[topic].push({ callback, priority });
// Sort by priority (higher priority first)
this.topics[topic].sort((a, b) => b.priority - a.priority);
return { topic, callback };
}
publish(topic, data) {
if (!this.topics[topic]) return;
this.topics[topic].forEach(({ callback }) => {
callback(data);
});
}
}
// Usage
pubSub.subscribe("event", handler1, 10); // Executes first
pubSub.subscribe("event", handler2, 5); // Executes second
pubSub.subscribe("event", handler3, 1); // Executes third
3. Wildcard Topics
Subscribe to multiple topics using patterns:
class PubSub {
// ... existing code ...
subscribe(pattern, callback) {
// Check if it's a wildcard pattern
if (pattern.includes("*")) {
const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "quot;);
// Store with special marker
if (!this.topics["__wildcards__"]) {
this.topics["__wildcards__"] = [];
}
this.topics["__wildcards__"].push({ pattern: regex, callback });
return { pattern, callback, isWildcard: true };
}
// Regular subscription
if (!this.topics[pattern]) {
this.topics[pattern] = [];
}
this.topics[pattern].push(callback);
return { topic: pattern, callback };
}
publish(topic, data) {
// Regular subscribers
if (this.topics[topic]) {
this.topics[topic].forEach((callback) => callback(data));
}
// Wildcard subscribers
if (this.topics["__wildcards__"]) {
this.topics["__wildcards__"].forEach(({ pattern, callback }) => {
if (pattern.test(topic)) {
callback(data);
}
});
}
}
}
// Usage
pubSub.subscribe("user:*", (data) => {
console.log("Any user event:", data);
});
pubSub.publish("user:login", { name: "John" });
pubSub.publish("user:logout", { name: "John" });
// Both trigger the wildcard subscriber
PubSub vs Observer Pattern
| Aspect | Observer Pattern | PubSub Pattern | | ----------------- | ----------------------------------------- | ------------------------------- | | Coupling | Direct coupling (subject knows observers) | Loose coupling (via topics) | | Communication | Direct method calls | Message-based | | Scalability | Limited by direct references | Highly scalable | | Flexibility | Less flexible | More flexible (wildcards, etc.) |
Benefits
- Decoupling: Publishers and subscribers don't know about each other
- Scalability: Easy to add new subscribers without modifying publishers
- Flexibility: Subscribers can be added/removed dynamically
- Maintainability: Changes to one component don't affect others
- Testability: Easy to test components in isolation
Use Cases
- Event-driven architectures: Microservices communication
- UI frameworks: Component communication
- Plugin systems: Plugin-to-core communication
- Real-time applications: WebSocket message routing
- Logging systems: Multiple log handlers
- Analytics: Tracking various events
Best Practices
- Use descriptive topic names:
"user:login"instead of"event1" - Document topics: Maintain a list of available topics
- Handle errors: Wrap callbacks in try-catch
- Clean up: Always unsubscribe when components are destroyed
- Avoid overuse: Don't use PubSub for simple direct communication
Common Pitfalls
- Memory leaks: Forgetting to unsubscribe
- Topic name collisions: Use namespacing (
"module:event") - Circular dependencies: Be careful with publish-subscribe cycles
- Performance: Too many subscribers can slow down publishing
Integration with Frameworks
React Example
// pubsub.js
export const pubSub = new PubSub();
// Component A
import { pubSub } from "./pubSub";
function ComponentA() {
const handleClick = () => {
pubSub.publish("button:clicked", { id: 123 });
};
return <button onClick={handleClick}>Click me</button>;
}
// Component B
function ComponentB() {
useEffect(() => {
const subscription = pubSub.subscribe("button:clicked", (data) => {
console.log("Button was clicked:", data);
});
return () => pubSub.unsubscribe(subscription);
}, []);
return <div>Listening for clicks...</div>;
}
Summary
The Publisher/Subscriber pattern is a powerful design pattern for creating loosely coupled, scalable applications. It enables components to communicate without direct dependencies, making code more maintainable and testable. Use it when you need flexible, event-driven communication between components.