Javascript design patterns: PubSub pattern
Definition
Publish–subscribe is a messaging pattern where senders of messages, called publishers, do not program the messages to be sent directly to specific receivers, called subscribers, but instead categorize published messages into classes without knowledge of which subscribers if any, there may be. Similarly, subscribers express interest in one or more classes and only receive messages that are of interest, without knowledge of which publishers, if any, there are.
It sounds difficult and incomprehensible, but it is actually very simple. We encounter similar mechanisms almost every day during work. To bring closer and clarify what this looks like, let’s first look at a simple example.
button.addEventListener("click", () => console.log("Hello"));
We hooked the listener to a button. When the user clicks on it, the console prints “Hello”. What does this click represent? Well, it represents an event. The user can hover (also event) over the button, but nothing will happen. Only if he/she clicks it. That’s how the listener was set up. To listen only for button clicks.
Unlike the click event, which is linked to the button, the PubSub pattern provides the possibility that one component emits an event and the other receives the result of that event and reacts according to its logic.
Example: https://codesandbox.io/s/design-pattern-pubsub-2vds9z
In the example, we can see that by clicking one button we get a toast message. Note: this only applies to the first 4 clicks. I will explain why later.
PubSub class
interface Subscribers {
[key: string]: Array<(data: any) => void>;
}
let subscribers: Subscribers = {};
export default {
publish(event: string, data: Object) {
if (!subscribers[event]) {
return;
}
subscribers[event].forEach((subscriberCallback) =>
subscriberCallback(data)
);
},
subscribe(event: string, callback: (data: any) => void) {
let index: number;
if (!subscribers[event]) {
subscribers[event] = [];
}
index = subscribers[event].push(callback) - 1;
return {
unsubscribe() {
subscribers[event].splice(index, 1);
}
};
}
};
- subscribers — It represents an object that contains events as properties, and the property value is an array of functions (methods) that will be executed when that event occurs. In our case
interface Subscribers {
[key: string]: Array<(data: any) => void>;
}
subscribers = {
showToastMessage: [
function handler() {},
]
}
As you can see, by broadcasting this event, the n-th number of functions can be started, so you should be careful when to use this pattern and how to name the events. It is recommended to do it with placeholders and not directly with strings, as is the case with “click”.
- publish — a method that publishes/broadcasts an event with certain information (payload — this is optional). Takes all registered methods, executes them, passes them data (payload)
buttons.ts
showMessage = () => {
const { type, text } = this.button.dataset;
pubSub.publish(SHOW_TOAST_MESSAGE, {
type,
text
});
};
- subscribe — A method used by a component that wants to react to a certain event.
toasts.ts
this.event = pubSub.subscribe(SHOW_TOAST_MESSAGE, this.handler);
Basically, it says: When this event happens (SHOW_TOAST_MESSAGE) execute this function (handler).
buttons component emits an event.
toasts component receives payload and reacts on the event.
Previously, I mentioned above that only the first four clicks will display the toast message. Now I will explain why.
PubSub monitors the event, but also provides the possibility to stop monitoring the event at some point. That’s why we saved subscribe in this.event because it returns the unsubscribe method that allows us to stop following the event at a given moment.
if (this.counter === 4) {
this.event.unsubscribe();
}
When the number of clicks reaches 4, stop tracking button clicks.
Example: https://codesandbox.io/s/pub-sub-pattern-video-players-93wmq2
In this situation, we have several components that send an event (I’m playing a video), and one component subscribed to that event so that it could control which video files it has to stop in order to avoid a collision.
Conclusion
- loose coupling — component communication through an intermediary. Classes do not need to hold each other’s logic or state.
— publish — sends, emits result/data
— subscribe — registers callback functions that will be executed when a certain event is emitted - ease of development — Just one class with two methods that handle communication
- real-time communication — sending events and execution happens immediately
- scalability & reliability — we don’t have to worry about the number of publishers and subscribers. The addition logic has been created and is valid for all projects. communication is simple and logic is separate. If it is necessary to debug something, it should be related to the execution logic located at the subscriber or payload at the publisher.
- We can use it in situations, to send a toast message when the user has logged in, or to stop playing one media (audio, video) because we started a new media. We don’t need a video file and, for example, an audio file to be played at the same time. When the user uploads the wrong type of file you can show a toast message, …
- If everything was perfect, this approach would be used everywhere. However, it has several disadvantages.
— It’s global. It’s not just one component communicating with another. It is an event to which n components can react. And that number can grow quickly.
— The number of subscribers and publishers is arbitrary. Imagine that an event like ours has ten more functions that will be executed when the user clicks on the button. What is the probability of a collision in the logic of who should do what and when?
— Do not directly write the name of the event, put it as a placeholder.
const SHOW = 'showSomething';
- The publisher is not aware of the subscriber, just as the subscriber is not aware of the publisher.