While working with any language, we tend to use several reusable design solutions to commonly occurring problems. In JavaScript, too, we have a mix of well-defined patterns.
The Observer pattern is one of them.
In this article, we shall understand more about the Observer design pattern in JavaScript and implement a small example in vanilla JavaScript.
What Is the Observer Design Pattern?
The Observer pattern follows a subscription model. A subscriber (commonly referred to as the observer) subscribes to an event or an action handled by a publisher (commonly referred to as the subject) is notified when the event or action occurs.
The subject broadcasts the occurrence of the event or action to all the observers.
When the observer no longer wishes to be notified of the changes by the subject, it unsubscribes itself from the subject, and the subject then removes it from the list of subscribers.
An Observer design pattern is very similar to a Publisher/Subscriber pattern, with a small difference that a Publisher/Subscriber pattern also specifies a topic it wants to subscribe to.
For example, when detecting keyboard shortcuts, the subscriber can choose to specify a key combination that it wants to listen to in a Publisher/Subscriber model.
Implementation of the Observer Pattern
As an example of the Observer pattern, we will go about implementing a simple interaction where multiple elements listen to the mouse position on the screen and perform different actions.
Below is an example of what our interaction looks like:
Before we implement this interaction, let us analyze what is happening in this example as the mouse position changes.
The mouse position is immediately updated in the textbox at the top right corner.
The circle follows the trajectory of the mouse after a delay of 1s.
From the above description, we see that multiple components need information about the same thing but behave differently.
From the above example, we identify that the subject listens to the mouse event on the window and relays it to whoever wants it. The circle and textbox are observers in the above example.
So let us now go ahead and implement it.
Step 1. Implement a MousePositionObservable class
As a first step let us go ahead and implement the MousePositionObservable
class. This class needs to do the following things:
Keep a list of observer callbacks.
Expose a
subscribe
method which the observers will call to subscribe to the change. The return value of this must be a function that will move thecallback
from the set ofsubscriptions
when called.Listen to
mouseMove
event and trigger all subscription callbacks.
The code looks like the below:
class MousePositionObservable {
constructor() {
this.subscriptions = [];
window.addEventListener('mousemove',this.handleMouseMove);
}
handleMouseMove = (e) => {
this.subscriptions.forEach(sub => sub(e.clientX, e.clientY));
}
subscribe(callback) {
this.subscriptions.push(callback);
return () => {
this.subscriptions = this.subscriptions.filter(cb => cb !== callback);
}
}
}
Step 2. Create HTML elements
We now create our HTML elements for circle
and textMessageBox
and add styles to them.
<div class="container">
<div class="circle" ></div>
<div class="mouse-position">
<h4>Mouse Position</h4>
<div class="position"></div>
</div>
</div>
.container {
position: relative;
width: 100vw;
height: 100vh;
background-color: #f3df49;
}
.circle {
position: absolute;
background-color: #238643;
width: 25px;
height: 25px;
border-radius: 50%;
z-index: 2;
}
.mouse-position {
position: fixed;
top: 20px;
right: 20px;
width: 200px;
height: 100px;
background-color: black;
border-radius: 4px;
padding: 4px 16px;
color: white;
}
.mouse-position h4 {
color: white;
margin: 10px 0;
}
Step 3. Add observers
The last step to make it come together is to create an instance of our MousePositionObservable
class and add observers to it.
To do that we shall invoke the subscribe
method on the class instance and pass on a callback.
Our code looks like the below:
const mousePositionObservable = new MousePositionObservable();
mousePositionObservable.subscribe((x, y) => {
const circle = document.querySelector('.circle');
window.setTimeout(() => {
circle.style.transform = `translate(${x}px, ${y}px)`;
}, 1000);
});
// Update the mouse positon container to show the mouse position values
mousePositionObservable.subscribe((x, y) => {
const board = document.querySelector('.mouse-position .position');
board.innerHTML = `
<div>
<div>ClientX: ${x}</div>
<div>ClientY: ${y}</div>
</div>
`
})
We add two subscriptions to the MousePositionObservable
instance, one for each element that needs to listen to mouse values.
The subscription callback for the circle
element gets the reference of the DOM element and updates its transform
property. The transform property will use hardware acceleration where possible, so using translate()
over position top and left will see performance benefits if any animations or transitions are also being used on the element.
The subscription callback for the textbox
element updates its HTML content by using the innerHTML
property.
Note: When you want to unsubscribe the listener, all you have to do is to store the returned value from the subscribe function call and invoke it like a function
That is all we need for our demo.
You can check out the working example in the Codepen below:
Advantages and Disadvantages of the Observer Design Pattern
An Observer design pattern provides us the following benefits:
It is extremely useful when we want to perform multiple actions on a single event.
It provides a way to decouple functionalities while maintaining consistency between related objects.
The downside of this pattern stems from its benefits:
- Since the Observer design pattern leads to loosely coupled code, it is sometimes hard to guarantee that other parts of the application are working as they should. For example, the subscriptions added to the subject may have code that is behaving incorrectly, but there is no way for the publisher to know that.
Real-World Applications
While working with web development we see that Redux
and React Context
are both examples of implementations built upon the Observer Design Pattern.
In Redux, we have a subscribe
method that allows us to add observers to the redux state which acts as the subject. Whoever subscribes to the redux store is notified when any change is made to the store.
Similarly, with React Context whenever the value is updated for the ContextProvider
, all components that subscribe to the Context either through the useContext
hook or through Context.Consumer
are re-rendered with updated context values.
Conclusion
In this article, we went through the Observer design pattern and how to use it within our application. We also implement a demo based on this pattern and learned about some of the advantages and disadvantages of following this approach to designing interactions.
Thank you for reading.
If you found this article useful and informative, please don't forget to like and share it with your friends and colleagues.
If you have any suggestions, please feel free to comment.
Follow me on Twitter for more web development content.
Top comments (11)
Maybe I'm reading this wrong, but I think there's a bug in your unsubscribe function. If the predicate in the filter is true, then the item won't be filtered. I think you want !==.
Thank you for pointing this out...it drove me crazy for an hour at least :)
You are correct, thanks for pointing it out. I have updated it in the post.
This is a really great article on the Observer pattern. To the point and very visual as well.
The bug mentioned by dev.to/shiznit013 "( .filter( !== instead of === )" was fixed in the first example but it still remains in the final example in Code-pen. This really drove me crazy for a few minutes till I read Shiznit013's comment.
But, other than that little bug, this is a beautiful piece. Thanks so much, for taking the time and effort to write this.
You might want to check out my servable observable :-)
servable.serveside.dev
Created it a couple years ago while reading the rx docs
It does need some polish
It does look really good 🙂
Thanks :-)
No word about RXJS which impléments this pattern for a long time…
Angular uses rxjs in his core :)
I would have loved to mention it but I haven't used RxJS or Angular.
I will definitely research and update the post in near future
Great content, I find it very 7
Thanks a lot. It feels good to hear this 😃