Photo by Everton Vila on Unsplash
JavaScript implementation of the Publish-Subscribe Pattern using ES6
A detailed guide to creating a message broker with the PubSub class
Table of contents
Publish-subscribe is a messaging pattern where publishers send messages to a message broker, which then delivers the message to subscribed consumers. This pattern allows for decoupling between the publisher and consumer, as the publisher does not need to know the specifics of who is consuming its messages.
In this post, we will walk through an implementation of the publish-subscribe pattern in JavaScript using ES6 syntax. We will be creating a PubSub
class that will handle the message broker functionality.
class PubSub {
constructor() {
this.subscribers = {};
}
subscribe(topic, callback) {
if (!this.subscribers[topic]) {
this.subscribers[topic] = [];
}
this.subscribers[topic].push(callback);
}
}
Next, let's define a publish
method that will be used by publishers to send messages to a particular topic. This method should loop through all the subscribers for that topic and execute their callback functions with the provided data:
class PubSub {
// ...
publish(topic, data) {
if (this.subscribers[topic]) {
this.subscribers[topic].forEach((callback) => {
callback(data);
});
}
}
}
Now we have the basic structure of our PubSub
class set up. Let's see how we can use it in practice.
First, we will create a new instance of our PubSub
class and define a few subscribers:
const pubsub = new PubSub();
const subscriber1 = (data) => {
console.log(`Received data: ${data}`);
};
const subscriber2 = (data) => {
console.log(`Received data: ${data}`);
};
pubsub.subscribe('topic1', subscriber1);
pubsub.subscribe('topic1', subscriber2);
Now we have two subscribers for the topic 'topic1'
. We can publish a message to this topic by using the publish
method:
pubsub.publish('topic1', 'Hello, world!');
Received data: Hello, world!
Received data: Hello, world!
We can also define a third subscriber for a different topic:
const subscriber3 = (data) => {
console.log(`Received data: ${data}`);
};
pubsub.subscribe('topic2', subscriber3);
If we publish a message to 'topic2'
, only subscriber3
will receive it:
Output:Received data: Hello, again!
To finish off our PubSub
class, let's add an unsubscribe
method that allows subscribers to unsubscribe from a particular topic. This method should remove the specified callback from the list of subscribers for that topic:
class PubSub {
// ...
unsubscribe(topic, callback) {
if (this.subscribers[topic]) {
this.subscribers[topic] = this.subscribers[topic].filter(
(cb) => cb !== callback
);
}
}
}
Now we have a fully functional PubSub
class that implements the publish-subscribe pattern in JavaScript. Here is the complete code:
class PubSub {
constructor() {
this.subscribers = {};
}
subscribe(topic, callback) {
if (!this.subscribers[topic]) {
this.subscribers[topic] = [];
}
this.subscribers[topic].push(callback);
}
publish(topic, data) {
if (this.subscribers[topic]) {
this.subscribers[topic].forEach((callback) => {
callback(data);
});
}
}
unsubscribe(topic, callback) {
if (this.subscribers[topic]) {
this.subscribers[topic] = this.subscribers[topic].filter(
(cb) => cb !== callback
);
}
}
}
With this implementation, publishers can send messages to a message broker (our PubSub
instance) and subscribed consumers will receive those messages. This allows for decoupling between the publisher and consumer and makes it easy to add or remove subscribers without affecting the publisher.
Scenario: Social media
Here is an example of how the PubSub
class could be used in a real-world scenario.
Imagine that we have a social media application with a feed that displays posts from the users that a user is following. We could use the publish-subscribe pattern to decouple the feed component from the logic for retrieving and displaying the posts.
First, let's define a Feed
class that subscribes to the 'new-post'
topic and adds the received post to the feed:
class Feed {
constructor() {
this.posts = [];
}
addPost(post) {
this.posts.unshift(post);
}
render() {
const feedContainer = document.getElementById('feed');
this.posts.forEach((post) => {
const postElement = document.createElement('div');
postElement.innerHTML = post.content;
feedContainer.appendChild(postElement);
});
}
}
Next, let's create a PostService
class that retrieves new posts from the server and publishes them to the 'new-post'
topic:
class PostService {
constructor() {
this.pubsub = new PubSub();
}
getNewPosts() {
// retrieve new posts from the server
const newPosts = [
{
id: 1,
content: 'Hello, world!',
},
{
id: 2,
content: 'This is my first post',
},
];
// publish each new post to the 'new-post' topic
newPosts.forEach((post) => {
this.pubsub.publish('new-post', post);
});
}
}
Finally, we can create a new Feed
instance and a new PostService
instance, and subscribe the feed to the 'new-post'
topic:
const feed = new Feed();
const postService = new PostService();
postService.pubsub.subscribe('new-post', (post) => {
feed.addPost(post);
feed.render();
});
postService.getNewPosts();
This will retrieve the new posts from the server and publish them to the 'new-post'
topic. The feed will receive the posts and add them to its list of posts, and then re-render the feed to display the new posts.
This allows for a separation of concerns between the Feed
class and the PostService
class, as the Feed
class does not need to know how the posts are retrieved, and the PostService
class does not need to know how the posts are displayed. The publish-subscribe pattern allows them to communicate through the message broker (our PubSub
instance) and remain decoupled.
Scenario: Task management
Imagine that we have a task management application with a calendar that displays the due dates for tasks. We could use the publish-subscribe pattern to decouple the calendar component from the logic for retrieving and displaying the tasks.
First, let's define a Calendar
class that subscribes to the 'new-task'
topic and adds the received task to the calendar:
class Calendar {
constructor() {
this.tasks = [];
}
addTask(task) {
this.tasks.push(task);
}
render() {
const calendarContainer = document.getElementById('calendar');
this.tasks.forEach((task) => {
const taskElement = document.createElement('div');
taskElement.innerHTML = `${task.name} - ${task.dueDate}`;
calendarContainer.appendChild(taskElement);
});
}
}
Next, let's create a TaskService
class that retrieves new tasks from the server and publishes them to the 'new-task'
topic:
class TaskService {
constructor() {
this.pubsub = new PubSub();
}
getNewTasks() {
// retrieve new tasks from the server
const newTasks = [
{
id: 1,
name: 'Task 1',
dueDate: '01/01/2024',
},
{
id: 2,
name: 'Task 2',
dueDate: '01/02/2024',
},
];
// publish each new task to the 'new-task' topic
newTasks.forEach((task) => {
this.pubsub.publish('new-task', task);
});
}
}
Finally, we can create a new Calendar
instance and a new TaskService
instance, and subscribe the calendar to the 'new-task'
topic:
const calendar = new Calendar();
const taskService = new TaskService();
taskService.pubsub.subscribe('new-task', (task) => {
calendar.addTask(task);
calendar.render();
});
taskService.getNewTasks();
This will retrieve the new tasks from the server and publish them to the 'new-task'
topic. The calendar will receive the tasks and add them to its list of tasks, and then re-render the calendar to display the new tasks.
This allows for a separation of concerns between the Calendar
class and the TaskService
class, as the Calendar
class does not need to know how the tasks are retrieved, and the TaskService
class does not need to know how the tasks are displayed. The publish-subscribe pattern allows them to communicate through the message broker (our PubSub
instance) and remain decoupled.
This covers the basics of implementing the publish-subscribe pattern in JavaScript with the PubSub
class. The publish-subscribe pattern is a powerful way to decouple components in a system and can be used in a variety of real-world scenarios.
I hope this post has helped understand how the publish-subscribe pattern works and how it can be implemented in JavaScript. Let's conclude the post here. If you have any further questions, feel free to ask.