Key Takeaways
- The Observer design pattern in JavaScript allows for one-to-many data binding between elements, which is especially useful for keeping multiple elements in sync with the same data.
- The Observer pattern consists of three main methods: subscribe, which adds new observable events; unsubscribe, which removes observable events; and broadcast, which executes all events with bound data.
- The Observer pattern can be effectively implemented using ES6 features, such as classes, arrow functions, and constants, making the code more succinct and reusable.
- The Observer pattern can be used to solve real-world problems in JavaScript, such as keeping a word count in a blog post updated with every keystroke, and can be further enhanced to build new features.
The Event Observer
A high-level view of the pattern looks like this:EventObserver
│
├── subscribe: adds new observable events
│
├── unsubscribe: removes observable events
|
└── broadcast: executes all events with bound data
After I flesh out the observer pattern I’ll add a word count that uses it. The word count component will take this observer and bring it all together.
To initialize the EventObserver
do:
class EventObserver {
constructor() {
this.observers = [];
}
}
Start with an empty list of observed events, and do this for every new instance. From now on, let’s add more methods inside EventObserver
to flesh out the design pattern.
The Subscribe Method
To add new events do:subscribe(fn) {
this.observers.push(fn);
}
Grab the list of observed events and push a new item to the array. The list of events is a list of callback functions.
One way to test this method in plain JavaScript is as follows:
// Arrange
const observer = new EventObserver();
const fn = () => {};
// Act
observer.subscribe(fn);
// Assert
assert.strictEqual(observer.observers.length, 1);
I use Node assertions to test this component in Node. The exact same assertions exist as Chai assertions too.
Note the list of observed events consists of humble callbacks. We then check the length of the list and assert that the callback is on the list.
The Unsubscribe Method
To remove events do:unsubscribe(fn) {
this.observers = this.observers.filter((subscriber) => subscriber !== fn);
}
Filter out from the list whatever matches the callback function. If there is no match, the callback gets to stay on the list. The filter returns a new list and reassigns the list of observers.
To test this nice method, do:
// Arrange
const observer = new EventObserver();
const fn = () => {};
observer.subscribe(fn);
// Act
observer.unsubscribe(fn);
// Assert
assert.strictEqual(observer.observers.length, 0);
The callback must match the same function that’s on the list. If there is a match, the unsubscribe method removes it from the list. Note the test uses the function reference to add and remove it.
The Broadcast Method
To call all events do:broadcast(data) {
this.observers.forEach((subscriber) => subscriber(data));
}
This iterates through the list of observed events and executes all callbacks. With this, you get the necessary one-to-many relationship to the subscribed events. You pass in the data
parameter which makes the callback data bound.
ES6 makes the code more effective with an arrow function. Note the (subscriber) => subscriber(data)
function that does most of the work. This one-liner arrow function benefits from this short ES6 syntax. This is a definite improvement in the JavaScript programming language.
To test this broadcast method, do:
// Arrange
const observer = new EventObserver();
let subscriberHasBeenCalled = false;
const fn = (data) => subscriberHasBeenCalled = data;
observer.subscribe(fn);
// Act
observer.broadcast(true);
// Assert
assert(subscriberHasBeenCalled);
Use let
instead of a const
so we can change the value of the variable. This makes the variable mutable which allows me to reassign its value inside of the callback. Using a let
in your code sends a signal to fellow programmers that the variable is changing at some point. This adds readability and clarity to your JavaScript code.
This test gives me the confidence necessary to ensure the observer is working as I expect. With TDD, it is all about building reusable code in plain JavaScript. There are benefits to writing testable code in plain JavaScript. Test everything, and retain what is good for code reuse.
With this, we have fleshed out the EventObserver
. The question is, what can you build with this?
The Observer Pattern in Action: A Blog Word Count Demo
For the demo, time to put in place a blog post where it keeps the word count for you. Every keystroke you enter as input will get synced by the observer design pattern. Think of it as free text input where every event fires an update to where you need it to go. To get a word count from free text input, one can do:const getWordCount = (text) => text ? text.trim().split(/\s+/).length : 0;
Done! There is a lot going on in this seemingly simple pure function, so how about a humble unit test? This way it is clear what I intended this to do:
// Arrange
const blogPost = 'This is a blog \n\n post with a word count. ';
// Act
const count = getWordCount(blogPost);
// Assert
assert.strictEqual(count, 9);
Note the somewhat wacky input string inside blogPost
. I intend for this function to cover as many edge cases as possible. As long as it gives me a proper word count we are heading, in fact, in the right direction.
As a side note, this is the real power of TDD. One can iterate on this implementation and cover as many use cases as possible. The unit test tells you how I expect this to behave. If the behavior has a flaw, for any reason, it is easy to iterate and tweak it. With the test, there is enough evidence left behind for any other person to make changes.
Time to wire up these reusable components to the DOM. This is the part where you get to wield plain JavaScript and weld it right into the browser.
A way to do it would be to have the following HTML on the page:
<textarea id="blogPost" placeholder="Enter your blog post..." class="blogPost">
</textarea>
Followed up by this JavaScript:
const wordCountElement = document.createElement('p');
wordCountElement.className = 'wordCount';
wordCountElement.innerHTML = 'Word Count: <strong id="blogWordCount">0</strong>';
document.body.appendChild(wordCountElement);
const blogObserver = new EventObserver();
blogObserver.subscribe((text) => {
const blogCount = document.getElementById('blogWordCount');
blogCount.textContent = getWordCount(text);
});
const blogPost = document.getElementById('blogPost');
blogPost.addEventListener('keyup', () => blogObserver.broadcast(blogPost.value));
Take all your reusable code and put in place the observer design pattern. This will track changes in the text area and give you a word count right beneath it. I’m using the body.appendChild()
in the DOM API to add this new element. Then, attaching the event listeners to bring it to life.
Note with arrow functions it is possible to wire up one-liner events. In fact, you broadcast event-driven changes to all subscribers with this. The () => blogObserver.broadcast()
does the bulk of the work here. It even passes in the latest changes to the text area right into the callback function. Yes, client-side scripting is super cool.
No demo is complete without one you can touch and tweak, below is the CodePen:
See the Pen The Observer Pattern by SitePoint (@SitePoint) on CodePen.
Now, I would not call this feature complete. It is but a starting point of the observer design pattern. The question in my mind is, how far are you willing to go?Looking Ahead
It is up to you to take this idea even further. There are many ways you can use the observer design pattern to build new features. You can enhance the demo with:- Another component that counts the number of paragraphs
- Another component that shows a preview of entered text
- Enhance the preview with markdown support, for example
Conclusion
The observer design pattern can help you solve real-world problems in JavaScript. This solves the perennial problem of keeping a bunch of elements synced with the same data. As often is the case, when the browser fires specific events. I’m sure most of you by now have come across such a problem and have run towards tools and third-party dependencies. This design pattern equips you to go as far as your imagination is willing to go. In programming, you abstract the solution into a pattern and build reusable code. There is no limit to how far this will take you. I hope you see how much, with a little discipline and effort, you can do in plain JavaScript. The new features in the language, such as ES6, help you write some succinct code that is reusable. This article was peer reviewed by Giulio Mainardi. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!Frequently Asked Questions (FAQs) about JavaScript Observer Pattern
How does the Observer Pattern work in JavaScript?
The Observer Pattern in JavaScript is a design pattern that facilitates one-to-many dependency between objects. This means that when one object changes its state, all its dependent objects are notified and updated automatically. This is achieved by using subjects and observers. The subject maintains a list of observers and facilitates adding or removing observers. The observers are basically objects that wish to be informed about changes in the subject. When a change occurs in the subject, it broadcasts to all registered observers.
What are the benefits of using the Observer Pattern in JavaScript?
The Observer Pattern promotes a loose coupling between objects which can lead to code that is more flexible and easier to maintain. It allows for a dynamic relationship between objects, where the subject and observers can interact without being tightly bound to each other. This pattern is particularly useful in event handling systems where an event can trigger changes in multiple objects.
Can you provide a simple example of the Observer Pattern in JavaScript?
Sure, here’s a basic example of the Observer Pattern in JavaScript:class Subject {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
fire(action) {
this.observers.forEach(observer => {
observer.update(action);
});
}
}
class Observer {
update(action) {
console.log(`Observer notified of ${action}`);
}
}
const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.fire('ACTION!');
How does the Observer Pattern differ from the Publisher-Subscriber Pattern?
While both patterns involve one-to-many dependencies, the key difference lies in how the subject (or publisher) communicates with its observers (or subscribers). In the Observer Pattern, the subject directly notifies its observers. In the Publisher-Subscriber Pattern, the publisher sends notifications to a mediator (or channel), which then pushes the notifications to the subscribers. This extra level of abstraction allows for greater flexibility and customization of the notification process.
Are there any drawbacks to using the Observer Pattern in JavaScript?
While the Observer Pattern has many benefits, it’s not without its drawbacks. One potential issue is the risk of memory leaks. This can occur if observers are not properly removed from the subject’s list of observers when they are no longer needed. Another potential issue is the difficulty in debugging or tracking the flow of data, especially in large applications, as the Observer Pattern can lead to complex and unpredictable interactions between objects.
How can I avoid memory leaks when using the Observer Pattern?
To avoid memory leaks, it’s important to remember to remove observers from the subject’s list of observers when they are no longer needed. This can be done using the unsubscribe method in the subject. For example:subject.unsubscribe(observer);
Can the Observer Pattern be used with other design patterns in JavaScript?
Yes, the Observer Pattern can be used in conjunction with other design patterns in JavaScript. For example, it can be combined with the Factory Pattern to create observers, or with the Singleton Pattern to ensure that only one instance of a subject exists.
Is the Observer Pattern supported in modern JavaScript frameworks?
Yes, many modern JavaScript frameworks, such as React and Vue, have built-in support for the Observer Pattern. For example, in React, the state of a component can be observed by other components, and changes to the state can trigger a re-render of the observing components.
How does the Observer Pattern relate to reactive programming in JavaScript?
The Observer Pattern is a key concept in reactive programming. In reactive programming, data is treated as a stream of events that can be observed and reacted to. The Observer Pattern provides the mechanism for setting up these observations and reactions.
Can the Observer Pattern be used in Node.js?
Yes, the Observer Pattern can be used in Node.js. In fact, Node.js’s event-driven architecture is a prime example of the Observer Pattern in action. Events in Node.js can be observed and handlers can be registered to react to these events, much like observers in the Observer Pattern.
Husband, father, and software engineer from Houston, Texas. Passionate about JavaScript and cyber-ing all the things.