20
How to Use Observables with Vanilla JavaScript
While working on a side project just for fun, I wanted to write a JavaScript script to call a REST API and eventually do some cool stuff on a webpage. It is purely vanilla JavaScript, with no fancy frameworks or even libraries being used.
First, I thought of using Promises for my calls and this was easy for me. I have done that a ton of times. However, it then hit me hard — why don’t I use Observables? I knew that vanilla JavaScript didn’t natively support Observables. But couldn’t I implement it myself? And that’s what I did.

The Observable itself would be of a new object type called Subject.
This Subject object should expose the subscribe and next functions.
subscribe should be called by observers to subscribe to the observable stream of data.
next should be called by the Subject owner to push/publish new data whenever available.
Additionally, I wanted the Subject owner to be able to know whenever no observers were interested in its data. This would enable the Subject owner to decide if he still wanted to get the data or not.
Also, the Subject owner should be able to know whenever at least one observer started being interested in its data. This would give the Subject owner more control on its data flow and any related operations.
Now back to the observer. He should be able to unsubscribe from the Subject at any time.
This leads us to a new object type called Subscription.
This Subscription object should expose an unsubscribe function.
unsubscribe should be called by the observer whenever he wants to stop listening to the data stream coming from the Subject.
Following these rules, I came up with the following implementation.

This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let Subscription = function(handlerId, unsubscribeNotificationCallback) { | |
let self = this; | |
self.unsubscribe = () => { | |
if(unsubscribeNotificationCallback) { | |
unsubscribeNotificationCallback(handlerId); | |
} | |
}; | |
return self; | |
}; |
Note that Subscription just notifies the Subject when the unsubscribe function is called.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let Subject = function(subscribersStateChangeNotificationCallback) { | |
let self = this; | |
let handlers = {}; | |
Object.defineProperty(self, "subscribersFound", { | |
get() { | |
let found = false; | |
for(const prop in handlers) { | |
if(handlers.hasOwnProperty(prop)) { | |
found = true; | |
break; | |
} | |
} | |
return found; | |
} | |
}); | |
Object.defineProperty(self, "subscribersCount", { | |
get() { | |
let count = 0; | |
for(const prop in handlers) { | |
if(handlers.hasOwnProperty(prop)) { | |
count++; | |
} | |
} | |
return count; | |
} | |
}); | |
let unsubscribeNotificationCallback = (handlerId) => { | |
if(handlerId && handlerId !== '' && handlers.hasOwnProperty(handlerId)) { | |
delete handlers[handlerId]; | |
if(subscribersStateChangeNotificationCallback && !self.subscribersFound) { | |
subscribersStateChangeNotificationCallback(false); | |
} | |
} | |
}; | |
self.subscribe = (handler) => { | |
let handlerId = createGuid(); | |
handlers[handlerId] = handler; | |
if(subscribersStateChangeNotificationCallback && self.subscribersCount === 1) { | |
subscribersStateChangeNotificationCallback(true); | |
} | |
return new Subscription(handlerId, unsubscribeNotificationCallback); | |
}; | |
self.next = (data) => { | |
for(const handlerId in handlers) { | |
handlers[handlerId](data); | |
} | |
}; | |
return self; | |
}; | |
let createGuid = function() { | |
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { | |
var r = Math.random()*16|0, v = c === 'x' ? r : (r&0x3|0x8); | |
return v.toString(16); | |
}); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
. | |
. | |
. | |
let subscribersStateChangeNotificationCallback = (subscriberFound) => { | |
if(!subscriberFound && isNowWatching) { | |
stopWatching(); | |
isNowWatching = false; | |
} else if(subscriberFound && !isNowWatching) { | |
startWatching(); | |
} | |
}; | |
self.data = new Subject(subscribersStateChangeNotificationCallback); | |
. | |
. | |
. | |
self.data.next(self.snapshot.data); | |
. | |
. | |
. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
. | |
. | |
. | |
const dashboardServiceSubscription = myDashboardService.data.subscribe((data) => { | |
... | |
}); | |
. | |
. | |
. | |
dashboardServiceSubscription.unsubscribe(); | |
. | |
. | |
. |

That’s it, everything worked like a charm and I was proud of what I achieved.
So, the punch line is that coding in vanilla JavaScript is not always equal to writing boring code, you can make it much more fun 😃
Hope you found reading this story as interesting as I found writing it.
This article was originally published here.
20