Diona Rodrigues

Observer and Pub-Sub Patterns for reactive behaviours in JavaScript

These are definitely two of my favourite and easiest to understand design patterns in JavaScript and here I'm going to share with you how they work and how to best use them in our code.

Posted on Aug 20, 2023 14 min to read
#Development
Diona Rodrigues
Diona Rodrigues - they/she
Front-end designer
Observer and Pub-Sub Patterns for reactive behaviours in JavaScript

Design patterns are typical conceptual solutions to common problems in software development, and most of them were thought of before JavaScript was even invented. So in this article, I'll show you how to use the Observer and Pub-Sub design patterns in JavaScript, but keep in mind that you can use them in many other programming languages as well.

In summary, the Observer and Pub-Sub patterns are very similar not only in concept but also in implementation. Both are used to allow asynchronous communication between components or services, triggering updates when any other event has been executed. For a better understanding, imagine that every time a state changes by clicking on a button, some parts of the application must be notified and updated, similar to what React does (in a very shallow example, of course!).

Summary:

  • The Observer pattern
  • The Pub-Sub pattern
  • Main differences between Observer and Pub-Sub Patterns
  • Some Pitfalls of Observer and Pub-Sub Patterns
  • References
  • Conclusion

Let's go deep!

The Observer Pattern

The Observer pattern is a design pattern that notifies multiple observers (listeners) when a common state has changed. So it creates a one-to-many relationship between objects without making the objects tightly coupled.

We can break the explanation of this pattern into three different parts:

  • Subject: is the observers manager. It maintains a list of listeners and notify them of each state change by using methods like add, remove and notify observers.
  • Observers: is an array of listeners that will be called by the notifier.
  • Notifier: is a method that, when executed, will run each of the observers passing the updated state to them.

Example:

Imagine an interface where, when logging in, the user sees their name in some areas of the page. So we need observers (listeners) to be able to be notified when the user is logged in, given the user's name as the state, and then update the required areas of the page by displaying the username.

/**
 * ./observers.js
 **/

// Array of observers
let observers = [];
// Method to add functions to observers array
export const addObserver = (observer) => {
  observers.push(observer);
};
// Method to remove a function from observers array
export const removeObserver = (observer) => {
  observers = observers.filter((obs) => obs !== observer);
};
// Method to run each of the observers passing the data to them
export const notifyObservers = (data) => {
  observers.forEach((observer) => observer(data));
};
/**
 * ./index.js
 **/

import { addObserver, notifyObservers } from './observers';
// First observer function
const updateSidebarUsername = (data) => {
  console.log(`Sidebar user name: ${data}`);
};
// Second observer function
const updateHeaderUsername = (data) => {
  console.log(`Header user name: ${data}`);
};
// Adding functions to observers
addObserver(updateSidebarUsername);
addObserver(updateHeaderUsername);
// Notifying the observers by passing the data to them
const btn = document.querySelector('button');
btn.addEventListener('click', () => notifyObservers('Diona'));

Play with this code on codesandbox.

Code explanation:

  1. Using module feature to isolate the functionality in a file, we first create the subject in ./observers.js. It contains the list of observers, methods to add and remove those observers, and also a function called notifyObservers() to run each of those observers by passing the updated state to them.

  2. Then in the ./index.js file we have two functions to illustrate the listeners (updateSidebarUsername() and updateHeaderUsername()), responsible for updating the username in two different areas called “sidebar” and “header”. Then we add these two listeners to the observers list using the addObserver() method.

  3. Next we have a button to illustrate the login functionality that when clicked will execute the notifyObservers() method passing the username as an argument, which will then call the two listeners from the observers list and display the two console.log().

I hope this short and simple example helps you understand how powerful this design pattern is, making code reactive without tight coupling between objects. The two observers don't know each other, but they both run every time the notifier is called by receiving the same state, which can be used to update different areas of the application when some event occurs.

In a real application, the method removeObserver() can be used to remove the listeners when necessary to avoid performance issues.

The Pub-Sub Pattern

The Publisher-Subscriber (Pub-Sub) pattern is part of the messaging patterns and its description is very similar to the Observer one:

“Enable an application to announce events to multiple interested consumers asynchronously, without coupling the senders to the receivers.” - Microsoft docs

The main difference relies not only on the terms used to describe it, but also because the Pub-Sub pattern is a many-to-many relationship. Here we can have multiple publishers sending different messages (data) to multiple subscribers without knowing each other, while in the Observer pattern we have only one state being sent to all listeners together (one-to-many relationship) where the subject directly controls all this process by itself.

We can also break the explanation of this pattern into three different parts:

  • Message Broker: is the bridge between publishers and subscribers. It contains all the methods needed to subscribe and unsubscribe listeners, and also to publish the messages.
  • Subscribers: list of subscribers (listeners) grouped by similarity as we can have multiple type of subscribers. So basically it will be an object where each prop (some devs call it channel or topic as well) will be an array containing a list of related listeners.
  • Publisher: a method that will take a prop name and the data so it can find the list of subscribers based on the prop name and iterate over them passing the data to each one.

The Pub-Sub pattern is a great solution for more complex and distributed architectures persisting messages for a high number of different clients, having different applications consuming data from it.

Some examples of its use are:

  • Real-time communication: financial companies, for instance, can use it to distribute real-time data to traders platforms subscribed to them.
  • Distributed Caching: the cache can be asynchronously seeded across multiple locations to reduce traffic and minimize the load on core services.
  • Multiple Data Sources: the Pub-Sub pattern is capable of collecting data from various sources, processing it and displaying it in a real-time dashboard, for example.

Example:

/**
 * ./pub-sub.js
 **/

// Object to store all subscribers
const subscribers = {};
// Method to subsribe functions by grouping them per eventName
export const subscribe = (eventName, callback) => {
  if (!subscribers[eventName]) {
    subscribers[eventName] = [];
  }
  subscribers[eventName].push(callback);
};
// Method to unsubscribe functions
export const unsubscribe = (eventName, callback) => {
  if (subscribers[eventName]) {
    subscribers[eventName] = subscribers[eventName].filter(
      (subscriber) => subscriber !== callback
    );
  }
};
// Method to publish data to subscribers
export const publish = (eventName, data) => {
  if (subscribers[eventName]) {
    subscribers[eventName].forEach((subscriber) => subscriber(data));
  }
};
/**
 * ./index.js
 **/

import { subscribe, publish } from './pub-sub';
// Event names for listeners grouping
const USER_EVENT_NAME = 'user-event-name';
const  = 'preferences-event-name';
// First user event subscriber function
const updateSidebarUsername = (data) => {
  console.log(`Sidebar user name: ${data}`);
};
// Second user event subscriber function
const updateHeaderUsername = (data) => {
  console.log(`Header user name: ${data}`);
};
// Subscribing user functions by grouping them together using the same event name
subscribe(USER_EVENT_NAME, updateSidebarUsername);
subscribe(USER_EVENT_NAME, updateHeaderUsername);
// Preferences event subscriber function
const updatePreferences = (data) => {
  console.table(data);
};
// Subscribing preferences function
subscribe(PREFERENCES_EVENT_NAME, updatePreferences);
// Publishing data to subscribers
const btn = document.querySelector('button');
btn.addEventListener('click', () => {
  publish(USER_EVENT_NAME, 'Diona');
  publish(PREFERENCES_EVENT_NAME, {
    themeColor: 'dark',
    currency: 'EUR',
  });
});

Play with this code on codesandbox.

Code explanation:

1- Using module feature to isolate the functionality in a file, we first create the message broker in ./pub-sub.js. It contains an object which will group lists of subscribers (listeners) by using arrays attached to the object props. It also contains methods to add and remove those subscribers, and a function called publish() to run each of those grouped subscribers by passing some data to them.

2- Then in the ./index.js file we have two const to store the event names (USER_EVENT_NAME and PREFERENCES_EVENT_NAME) - which will be used to group the subscribers - and three functions to illustrate those subscribers: updateSidebarUsername() and updateHeaderUsername() will be grouped using the same event name to run together as they are responsible for updating the username in two different areas called “sidebar” and “header”. And updatePreferences() which will be attached to a different event name and will be responsible to update the user preferences.

3- Then we add those three listeners to the subscribers object using the subscribe() method which will receive two arguments: the customised prop name and the subscriber functions.

4- Next is a button to illustrate an event action which when clicked will execute two publish() methods, the first will execute the subscribers related to the USER_EVENT_NAME property passing the username to them, and the other will execute the subscribe under PREFERENCES_EVENT_NAME passing an object containing the user preferences to it.

Main differences between Observer and Pub-Sub Patterns

Illustration on how Pub-Sub and Observer Patterns work.

The terms used to describe them

  • Observer pattern has terms like subject, observers, notifier.
  • Pub-Sub pattern: here we see message broker, subscribers and publishers.

The relationships between their objects

  • Observer pattern: is a one-to-many relationship, which means that the subject maintains a list of observers that will be called together every time the state changes.
  • Pub-Sub pattern: is many-to-many, where multiple publishers can send different messages to multiple subscribers through the message broker.

Control of listeners and data

  • Observer pattern: the subject is aware of its observers and knows how they need to receive the updated state and when they need to be called (on each state changes).
  • Pub-Sub pattern: here publishers have no idea about their subscribers and these two groups can exist and function without each other. The message broker works by bridging this gap.

When to use them

  • Observer pattern: its common use is when a state should be shared between different but related listeners.
  • Pub-Sub pattern: we use it when there is no relationship between publishers and subscribers and so we might have different data (messages) being sent between them.

Some Pitfalls of Observer and Pub-Sub Patterns

These two patterns are awesome, but you should know some pitfalls they can cause in JavaScript:

  • Memory leaks: observers and subscribers should always be removed from the subject and message broker when they are no longer needed to avoid memory leaks. That’s the importance of ‘removeObserver()’ and ‘unsubscribe()’ methods.
  • Infinity loop: happens when two or more objects depend on each other where when one is updated the other is called which will call the previous one and so on. It can also be called circular dependencies and you need to be aware of it because it might crash the application.
  • Performance issues: if there are many observers in the subject and/or many subscribers on the same publisher, performance can be negatively affected. One way to avoid this is to limit the number of listeners and optimize the code to handle it.

References

Conclusion

As you can see, the Observer and Pub-Sub design patterns work in a similar way, providing a way to have listeners that will be notified when some event is executed, triggering updates in the application (or on different different clients when it comes to the Pub-Sub pattern). However, you've also seen that they can be different when it comes to how they establish a relationship with other objects: the Observer pattern is a one-to-many relationship and has more control over listeners, while the Pub-Sub is many-to-many and there is no bound relationship between its publishers and subscribers.

In some scenarios, because of their similarities, developers tend to call them using the same name, sometimes as Observer pattern, sometimes as Pub-Sub pattern, but keep in mind that they can be different, as you've seen in this article.

Hope you learned how to use them, their main differences and pitfalls.

See you next time! 😁