How to implement pub/sub pattern in JavaScript
The publish-subscribe pattern decouples communication between components by allowing publishers to emit events without knowing who subscribes, and subscribers to listen for events without knowing who publishes. As the creator of CoreUI with 26 years of JavaScript development experience, I’ve implemented pub/sub systems in applications serving millions of users, enabling loosely coupled architectures that scale efficiently across distributed features.
The most maintainable approach uses a central event bus with typed event channels.
Basic Pub/Sub Implementation
class PubSub {
constructor() {
this.events = {}
}
subscribe(event, callback) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(callback)
// Return unsubscribe function
return () => {
this.events[event] = this.events[event].filter(cb => cb !== callback)
}
}
publish(event, data) {
if (!this.events[event]) {
return
}
this.events[event].forEach(callback => callback(data))
}
clear(event) {
if (event) {
delete this.events[event]
} else {
this.events = {}
}
}
}
// Usage
const pubsub = new PubSub()
// Subscribe to events
const unsubscribe = pubsub.subscribe('user:login', (user) => {
console.log('User logged in:', user.name)
})
// Publish events
pubsub.publish('user:login', { name: 'John', id: 1 })
// Unsubscribe
unsubscribe()
Singleton Event Bus
const EventBus = (() => {
const events = {}
return {
on(event, callback) {
if (!events[event]) {
events[event] = []
}
events[event].push(callback)
},
off(event, callback) {
if (!events[event]) return
events[event] = events[event].filter(cb => cb !== callback)
},
once(event, callback) {
const wrapper = (data) => {
callback(data)
this.off(event, wrapper)
}
this.on(event, wrapper)
},
emit(event, data) {
if (!events[event]) return
// Create copy to avoid issues if callbacks modify subscribers
[...events[event]].forEach(callback => {
try {
callback(data)
} catch (error) {
console.error(`Error in ${event} subscriber:`, error)
}
})
},
clear() {
Object.keys(events).forEach(event => {
events[event] = []
})
}
}
})()
// Usage
EventBus.on('cart:add', (product) => {
console.log('Added to cart:', product.name)
})
EventBus.once('cart:checkout', (order) => {
console.log('Checkout initiated:', order.id)
// Only fires once
})
EventBus.emit('cart:add', { name: 'Laptop', price: 999 })
EventBus.emit('cart:checkout', { id: 123, total: 999 })
Namespaced Events
class NamespacedPubSub {
constructor() {
this.events = {}
}
subscribe(event, callback) {
// Support namespaced events: 'user:login', 'user:logout'
const parts = event.split(':')
const namespace = parts[0]
const eventName = parts[1] || '*'
if (!this.events[namespace]) {
this.events[namespace] = {}
}
if (!this.events[namespace][eventName]) {
this.events[namespace][eventName] = []
}
this.events[namespace][eventName].push(callback)
return () => {
this.events[namespace][eventName] = this.events[namespace][eventName].filter(
cb => cb !== callback
)
}
}
publish(event, data) {
const parts = event.split(':')
const namespace = parts[0]
const eventName = parts[1]
if (!this.events[namespace]) return
// Call specific event subscribers
if (this.events[namespace][eventName]) {
this.events[namespace][eventName].forEach(cb => cb(data))
}
// Call wildcard subscribers (namespace:*)
if (this.events[namespace]['*']) {
this.events[namespace]['*'].forEach(cb => cb({ event: eventName, data }))
}
}
clearNamespace(namespace) {
delete this.events[namespace]
}
}
// Usage
const pubsub = new NamespacedPubSub()
// Subscribe to specific event
pubsub.subscribe('user:login', (user) => {
console.log('Login:', user.name)
})
// Subscribe to all user events
pubsub.subscribe('user:*', ({ event, data }) => {
console.log(`User event: ${event}`, data)
})
pubsub.publish('user:login', { name: 'John' })
pubsub.publish('user:logout', { name: 'John' })
Priority Subscribers
class PriorityPubSub {
constructor() {
this.events = {}
}
subscribe(event, callback, priority = 0) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push({ callback, priority })
// Sort by priority (higher priority first)
this.events[event].sort((a, b) => b.priority - a.priority)
return () => {
this.events[event] = this.events[event].filter(
sub => sub.callback !== callback
)
}
}
publish(event, data) {
if (!this.events[event]) return
for (const subscriber of this.events[event]) {
subscriber.callback(data)
}
}
}
// Usage
const pubsub = new PriorityPubSub()
// Low priority
pubsub.subscribe('data:save', (data) => {
console.log('3. Update UI')
}, 0)
// High priority (runs first)
pubsub.subscribe('data:save', (data) => {
console.log('1. Validate data')
}, 100)
// Medium priority
pubsub.subscribe('data:save', (data) => {
console.log('2. Save to server')
}, 50)
pubsub.publish('data:save', { id: 1 })
// Output:
// 1. Validate data
// 2. Save to server
// 3. Update UI
Async Pub/Sub
class AsyncPubSub {
constructor() {
this.events = {}
}
subscribe(event, callback) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(callback)
return () => {
this.events[event] = this.events[event].filter(cb => cb !== callback)
}
}
async publish(event, data) {
if (!this.events[event]) return
const results = []
for (const callback of this.events[event]) {
try {
const result = await callback(data)
results.push({ success: true, result })
} catch (error) {
results.push({ success: false, error })
}
}
return results
}
async publishParallel(event, data) {
if (!this.events[event]) return []
const promises = this.events[event].map(callback =>
Promise.resolve(callback(data))
.then(result => ({ success: true, result }))
.catch(error => ({ success: false, error }))
)
return Promise.all(promises)
}
}
// Usage
const pubsub = new AsyncPubSub()
pubsub.subscribe('user:create', async (user) => {
await saveToDatabase(user)
return { saved: true }
})
pubsub.subscribe('user:create', async (user) => {
await sendWelcomeEmail(user)
return { emailSent: true }
})
// Sequential execution
const results = await pubsub.publish('user:create', {
name: 'John',
email: '[email protected]'
})
// Parallel execution
const parallelResults = await pubsub.publishParallel('user:create', {
name: 'Jane',
email: '[email protected]'
})
Event History and Replay
class PubSubWithHistory {
constructor(maxHistory = 100) {
this.events = {}
this.history = {}
this.maxHistory = maxHistory
}
subscribe(event, callback, replayHistory = false) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(callback)
// Replay historical events if requested
if (replayHistory && this.history[event]) {
this.history[event].forEach(data => callback(data))
}
return () => {
this.events[event] = this.events[event].filter(cb => cb !== callback)
}
}
publish(event, data) {
// Store in history
if (!this.history[event]) {
this.history[event] = []
}
this.history[event].push(data)
// Limit history size
if (this.history[event].length > this.maxHistory) {
this.history[event].shift()
}
// Publish to subscribers
if (this.events[event]) {
this.events[event].forEach(callback => callback(data))
}
}
getHistory(event) {
return this.history[event] || []
}
clearHistory(event) {
if (event) {
delete this.history[event]
} else {
this.history = {}
}
}
}
// Usage
const pubsub = new PubSubWithHistory(50)
pubsub.publish('chat:message', { text: 'Hello', user: 'John' })
pubsub.publish('chat:message', { text: 'Hi there', user: 'Jane' })
// New subscriber gets message history
pubsub.subscribe('chat:message', (msg) => {
console.log(`${msg.user}: ${msg.text}`)
}, true) // replayHistory = true
// Output:
// John: Hello
// Jane: Hi there
Filtered Subscriptions
class FilteredPubSub {
constructor() {
this.events = {}
}
subscribe(event, callback, filter = null) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push({ callback, filter })
return () => {
this.events[event] = this.events[event].filter(
sub => sub.callback !== callback
)
}
}
publish(event, data) {
if (!this.events[event]) return
this.events[event].forEach(({ callback, filter }) => {
if (!filter || filter(data)) {
callback(data)
}
})
}
}
// Usage
const pubsub = new FilteredPubSub()
// Only receive messages from specific user
pubsub.subscribe(
'chat:message',
(msg) => console.log('Admin message:', msg.text),
(msg) => msg.user === 'admin'
)
// Only receive high priority notifications
pubsub.subscribe(
'notification',
(notif) => console.log('URGENT:', notif.text),
(notif) => notif.priority === 'high'
)
pubsub.publish('chat:message', { user: 'admin', text: 'System update' })
// Output: Admin message: System update
pubsub.publish('chat:message', { user: 'john', text: 'Hello' })
// No output (filtered out)
pubsub.publish('notification', { priority: 'high', text: 'Server down' })
// Output: URGENT: Server down
Best Practice Note
This is how we implement event-driven communication in CoreUI applications for loosely coupled architecture. The pub/sub pattern decouples publishers from subscribers, enabling components to communicate without direct dependencies. Always implement unsubscribe functionality to prevent memory leaks, use namespaces to organize events, handle errors gracefully in subscribers to prevent one broken subscriber from affecting others, and consider async execution for I/O-heavy operations. For complex applications, add features like priority, filtering, and history replay.
For production applications, consider using CoreUI’s React Admin Template which includes event bus utilities for component communication.
Related Articles
For related patterns, check out how to implement observer pattern in JavaScript and how to implement event emitter in JavaScript.



