PubSub in JavaScript: Intro to Design Patterns

PubSub in JavaScript: Intro to Design Patterns

Behavioral design patterns in JavaScript
Ferenc AlmasiLast updated 2021 July 13 • Read time 19 min read
Get an intro to design patterns in JavaScript. We will take a look how you can create your own pubsub (Publish-Subscribe) library...
  • twitter
  • facebook
JavaScript

When we are talking about design patterns, we are referring to a solution to a commonly occurring problem in software development.

Patterns are about reusability which helps us resolve architectural problems that can occur throughout our program, regardless of the language it is implemented in. Therefore they are not tied to JavaScript specifically, they are language independent.

Design patterns are categorized into three different types. These are called Creational, Structural and Behavioural patterns. We have a ton of different ones in each type. The one we are interested in this article is called Publish-Subscribe or PubSub for short, also more commonly referred to as Observer.

They are often used interchangeably but we have to differentiate between the two because they are not exactly the same. But before diving into the differences, let’s see what kind of problem is it supposed to solve?


The Problem

The problem arises when we have different modules throughout our application and we want to share some data between them, yet we also don’t want them to be directly dependent on each other. We need a link to create a communication channel.

To give you a practical example, say we have an e-commerce site where products are being sold. We have filtersModule.js which handles the filtering functionality and this is displayed on multiple pages; listView.js and gridView.js. Whenever the filters are updated we want to notify both files about the change so we can update the list of products accordingly. This can be solved in many ways, one of them being the use of PubSub or Observer pattern. Now back to the differences.


Observer vs PubSub

To understand the differences between the two, we need to go over some naming convention first.

Let’s start with the Observer. In the observer pattern, we have a subject which keeps track of its dependents, called observers. The subject can notify them of any state change and observers in return can execute blocks of code for us.

In PubSub, we have a sender called publisher and unlike in the Observer pattern, they don’t hold any reference to their observers, called subscribers, nor they are programmed to send messages directly to predefined receivers.

To emphasis the core difference between the two: in the Observer pattern, observers and subjects are aware of each other and they are linked together. Specific messages are being sent to specific observers, while in PubSub, publishers and subscribers don’t need to know about each other. Any subscription can be made to any published event. In terms of flexibility, PubSub is more compliant.

If you are interested in more design patterns, there’s a really neat collection of them in dofactory.com with definitions, diagrams, and in-depth examples.

Looking to improve your skills? Check out our interactive course to master JavaScript from start to finish.
Master JavaScriptinfo Remove ads

The Blueprint

Let’s dive into coding. First, let’s see what we want as an end result.

Copied to clipboard!
// Publish an event, whenever a filter is changed and pass the changed data
EventService.publish('filterUpdated', data);
filtersModule.js

Whenever a filter is changed, we want to publish an event inside filtersModule.js called filterUpdated, passing in a data object that contains all data related to changed filters.

Copied to clipboard! Playground
EventService.subscribe('filterUpdated', (data) => {
  console.log('Filters have been updated with', data);
});

// Later in our application, whenever we navigate to a different view that don't display the filters
// we can stop listening for any change.
EventService.unsubscribe('filterUpdated');
gridView.js

Inside gridView.js we listen to this event by subscribing to it and we want to execute a callback function where we operate on the received data that is being published. Later in our application, whenever the user navigates to a page that doesn’t have filtering, we also want to remove the subscription by unsubscribing from the filterUpdated event. That being said, we can already see what methods we are going to need.


The Skeleton

It’s clear from the end result that we want an EventService object, with three different methods; publish, subscribe and unsubscribe. But how will these three methods work together logically to create the behavior that we want?

Basically we want to have a list of subscriptions where we keep track of every subscribe event. We also want to have an associated callback function, which should be passed as a second parameter to EventService.subscribe(). Whenever we call publish with the same event name, we simply execute the function associated with it. Unsubscribing from an event simply means we want to remove it from our list of subscriptions. To keep track of every event, we are going to use an additional property. Let’s call it subscriptions.

With that in mind, let’s lay out the base structure:

Copied to clipboard! Playground
const EventService = {

    // List of subscriptions currently in use
    subscriptions: {},

    /**
     * @method publish
     *
     * @param event { string } - name of the event to publish
     * @param data  { any }    - optional data to pass on to subscription
     *
     * @example
     * - event.publish('event');
     * - event.publish('event', { data: 'customData' });
     * - event.publish('event event1 event2', { data: 'customData' });
     */
    publish(event, data?) {
      
    },

    /**
     * @method subscribe
     *
     * @param event    { string } 	- name of the event to subscribe to
     * @param callback { function } - function callback to execute once the event has been published
     *
     * @example
     * - event.subscribe('event', function () { ... });
     * - event.subscribe('event event1 event2', function (data) { ... });
     * - event.subscribe('event.namespaced', function (data) { ... });
     */
    subscribe(event, callback) {
      
    },

    /**
     * @method unsubscribe
     *
     * @param event { string } - name of the event to unsubscribe from
     *
     * @example
     * - event.unsubscribe('event');
     * - event.unsubscribe('event event1 event2');
     * - event.unsubscribe('event.namespaced');
     */
    unsubscribe(event) {
      
    }
};

module.exports = EventService;
EventService.js

Just to make it a little bit more flexible, say we also want to handle multiple event subscriptions and events with namespaces. Namespacing an event can be useful when we have multiple subscriptions to the same event, but we want different functionality in each subscriber and we also want to easily keep track of them. You can see this functionality used in the examples above the subscribe method.

From now on, we can start implementing each method one by one, starting with probably the most difficult piece, which is the handling of subscribing to events.


Subscribing to Events

Whenever we do a subscription, all we want to do is create a new property with the name of the event inside our subscriptions object. Starting with some simple declarations and prechecks:

Copied to clipboard! Playground
subscribe(event, callback) {
    const events = event.split(' ');
    let eventArray = [];

    if (!callback) {
        throw new Error('callback is missing from subscribe: ' + event);
    }
}
EventService.js

We first need to grab the events from the parameter. As seen from the examples, we can expect it to be a list of events separated by white spaces. So to collect all events we simply split the event argument by a white space. We also need to take care of namespaced events. For that we are going to use the eventArray variable. Lastly, we do a small check; the callback is mandatory so if there’s none provided, we are going to throw an error.

The next step is to actually loop through our events array and create a node under subscriptions with the event name itself.

Copied to clipboard! Playground
// That means in case of
EventService.subscribe('event event1 event2.firstEvent event2.secondEvent', () => {...});

// We are expecting EventService.subscriptions to be
{
  event: [
    { callback: ƒ, namespace: undefined }
  ],
  event1: [
    { callback: ƒ, namespace: undefined }
  ],
  event2: [
    {callback: ƒ, namespace: 'firstEvent'}
    {callback: ƒ, namespace: 'secondEvent'}
  ]
}

// Where the callback is the function passed as the second parameter to EventService.subscribe
EventService.js

In case of no namespace provided, we can leave its value to be undefined. Based on that, we can extend our subscribe method with the following:

Copied to clipboard! Playground
subscribe(event, callback) {
    const events = event.split(' ');
    let eventArray = [];

    if (!callback) {
        throw new Error('callback is missing from subscribe: ' + event);
    }

    for (const singleEvent of events) {
        if (singleEvent.indexOf('.') > -1) {
            eventArray = singleEvent.split('.');
        }

        if (!this.subscriptions[eventArray[0] || singleEvent]) {
            this.subscriptions[eventArray[0] || singleEvent] = [];
        }
        
        for (const currentInstance of this.subscriptions[eventArray[0] || singleEvent]) {
            if (currentInstance.namespace === eventArray[1]) {
                return;
            }
        }
    }
}
EventService.js

Again we do some precautions. We first check on line:10 if the current index is a namespaced event. In that case, we can fill our eventArray variable with it, eg.: event.namespace becomes ['event', 'namespace'].

Next, we need to check whether we already have a subscription to the event. This is what we do on line:14. If there’s none, we create an empty array for it. For the name we either use eventArray[0], which holds the base name for a namespaced event, or in case it is undefined, we use singleEvent. Now whenever we do subscribe('event') we should get an event property on the subscriptions object.

As a last step from line:18, we want to make sure that namespaces are unique and they are not overridden by mistake. By looping through our subscriptions list and checking if one of the instances namespace matches the one in our passed event parameter, we can eliminate the following case:

Copied to clipboard! Playground
// Creating a namespaced event
EventService.subscribe('loaded.custom', () => {
  console.log('call has been made');
});

// Later in our application we accidentally use the same namespace for the same subscription
EventService.subscribe('loaded.custom', () => {
  console.log('call has been overriden');
});

// We then publish the event
EventService.publish('loaded');

// output will still be "call has been made"
EventService.js

All that’s left to do is to create the subscription’s body, which can be done in 3 lines of code:

Copied to clipboard! Playground
subscribe(event, callback) {
    const events = event.split(' ');
    let eventArray = [];

    if (!callback) {
        throw new Error('callback is missing from subscribe: ' + event);
    }

    for (const singleEvent of events) {
        if (singleEvent.indexOf('.') > -1) {
            eventArray = singleEvent.split('.');
        }

        if (!this.subscriptions[eventArray[0] || singleEvent]) {
            this.subscriptions[eventArray[0] || singleEvent] = [];
        }

        for (const currentInstance of this.subscriptions[eventArray[0] || singleEvent]) {
            if (currentInstance.namespace === eventArray[1]) {
                return;
            }
        }

        this.subscriptions[eventArray[0] || singleEvent].push({
            callback,
            namespace: eventArray[1]
        });

        eventArray = [];
    }
}
EventService.js

This is what we have done from line:24 till line:27. We create a new object and give it a callback property which will be the callback provided in the arguments of the function. We also give it a namespace property which will either be undefined or the namespace we provided for the event. As a last step, we empty out the eventArray variable, so we can reuse it in the next iteration. Now we can start moving onto the implementation of the unsubscribe functionality.


Unsubscribing From Events

Just as for subscribing, we can start with the same base for unsubscribing:

Copied to clipboard! Playground
unsubscribe(event) {
    const events = event.split(' ');
    let eventArray;

    for (const currentEvent of events) {
        eventArray = currentEvent.split('.') || currentEvent;
    }
}
EventService.js

We first create an events array, based on the event parameter and we also declare an eventArray variable, without any value this time. We then start looping over our events and assign the current event name to eventArray at each iteration.

Next, we need to check whether the subscription exists and if so, we need to loop through the event, since we can have multiple subscriptions to the same event. This leaves us with:

Copied to clipboard! Playground
unsubscribe(event) {
    const events = event.split(' ');
    let eventArray;

    for (const currentEvent of events) {
        eventArray = currentEvent.split('.') || currentEvent;

        if (this.subscriptions[eventArray[0]]) {
            for (let j = 0; j < this.subscriptions[eventArray[0]].length; j++) {
            
            }
        }
    }
}
EventService.js

On line:8 we check if the event we want to unsubscribe from actually exists. Since we split the event argument we got on line:2, we can ensure that currentEvent will be an array, hence we are referring to the event name as eventArray[0], even if we don’t use split on the currentEvent variable. After that, we can start looping on it. This is needed as we might only want to remove a namespaced instance and not the whole subscription itself. To actually check whether we are about to remove a namespaced event or not, we need an if statement to check if eventArray[1] exists. If it is, we know we are unsubscribing from a namespaced event.

Copied to clipboard! Playground
unsubscribe(event) {
    const events = event.split(' ');
    let eventArray;

    for (const currentEvent of events) {
        eventArray = currentEvent.split('.') || currentEvent;

        if (this.subscriptions[eventArray[0]]) {
            for (let j = 0; j < this.subscriptions[eventArray[0]].length; j++) {
                if (eventArray[1]) {
                    // Logic for unsubscribing from namespaced event
                } else {
                    delete this.subscriptions[eventArray[0]];
                    break;
                }
            }
        }
    }
}
EventService.js

Otherwise, we can just delete the node in this.subscriptions. We also need to break after that to jump out from the loop. All that’s left to do is to check for the namespace of the current subscription’s instance, and if it matches with the value inside eventArray[1], we just splice the array at the index of j. This is what has been done on line:13.

Copied to clipboard! Playground
unsubscribe(event) {
    const events = event.split(' ');
    let eventArray;

    for (const currentEvent of events) {
        eventArray = currentEvent.split('.') || currentEvent;

        if (this.subscriptions[eventArray[0]]) {
            for (let j = 0; j < this.subscriptions[eventArray[0]].length; j++) {
                if (eventArray[1]) {

                    if (this.subscriptions[eventArray[0]][j].namespace === eventArray[1]) {
                        this.subscriptions[eventArray[0]].splice(j, 1);
                    }

                    if (!this.subscriptions[eventArray[0]].length) {
                        delete this.subscriptions[eventArray[0]];
                        break;
                    }

                } else {
                    delete this.subscriptions[eventArray[0]];
                    break;
                }
            }
        }
    }
}
EventService.js

I also included another if statement, starting from line:16. Its job is to determine if there’s only one instance left for a given subscription. If we are deleting that sole instance, we can essentially remove the whole event itself, meaning:

Copied to clipboard! Playground
// The contents of this.subscriptions
{
  loaded: [
    { callback: ƒ, namespace: 'custom' }  
  ]
}

// Calling it will remove `this.subscriptions.loaded` altogether
// Since we don't have any other instances
EventService.unsubscribe('loaded.custom');
EventService.js

As a last step, all we have to do now is implement the functionality for publishing our events so their corresponding callback function can be fired.

Looking to improve your skills? Check out our interactive course to master JavaScript from start to finish.
Master JavaScriptinfo Remove ads

Publishing Events

If you made it this far, congratulations, you are over the hard part! 🎉Probably the simplest piece in the whole equation is publishing events. All we have to do is loop through our subscription list and if we find the passed in event’s name, we execute its callback property with the passed in data.

Copied to clipboard! Playground
publish(event, data?) {
    for (const key in this.subscriptions) {
        if (this.subscriptions.hasOwnProperty(key)) {
            if (key === event) {
                for (const currentInstance of this.subscriptions[key]) {
                    currentInstance.callback(data);
                }
            }
        }
    }
}
EventService.js

For looping, we can use a simple for ... in statement. To make sure we only iterating through its own properties, we can wrap everything inside an if. This is what line:3 is supposed to do. Then on line:4, we can check if the passed in event matches with the current key and if so, call the callback property and pass in data that we get from the second argument of the function.

If everything is done correctly, running EventService.publish('filterUpdated', data); in your console should give you a console.log with the passed in data immediately.

By now, you should have a solid understanding of how a Publish-Subscribe behavioral design pattern can be achieved. Congratulations for reaching this far! 🙌 Before closing off, I would like to give you some advice about design patterns in general.


Things to Keep in Mind

First and foremost, don’t make patterns a hammer looking for nails. What can be solved without introducing a new pattern, should be solved.

Everything comes with a trade-off, the same applies to patterns as well. While it can help you in a lot of cases, on the downside, it increases your code complexity and your overall bundle size.

You should always strive to keep your code as simple as possible, (Remember the KISS principle — Keep it simple, stupid) and only use a pattern when you see a need for it.

If you do, however, don’t reinvent the wheel, use an already existing pattern as a solution. Now I’ll go ahead and publish this story. ☕️

  • twitter
  • facebook
JavaScript
Did you find this page helpful?
📚 More Webtips
Frontend Course Dashboard
Master the Art of Frontend
  • check Access 100+ interactive lessons
  • check Unlimited access to hundreds of tutorials
  • check Prepare for technical interviews
Become a Pro

Courses

Recommended

This site uses cookies We use cookies to understand visitors and create a better experience for you. By clicking on "Accept", you accept its use. To find out more, please see our privacy policy.