A best practice on how to define your Subjects in JavaScript objects.
While working on your cool JavaScript project, you might need to implement a service which manages a stream of data. In this case, the first thing pops up into your mind is using Observables or Subjects.
You can do this using libraries like Rxjs or you can even implement it yourself as I showed you before on my article How to Use Observables with Vanilla JavaScript. No frameworks used, just pure vanilla JavaScript.
Either ways, at the end you would need to wrap a Subject inside your service object. However, there are more than one way to do it.
In this article, we would get introduced to a Best Practice on how to implement this.
Time for Code
Let’s come up with a simple example. In our solution, we have an Authentication Service which handles logging in, logging out, getting current logged in user,….
We also have Some Module which does some interesting stuff and needs to know about the logged in user at some point.
We will implement this solution in simple steps and see where it goes.
In our solution, I would use my own implementation of the Subscription and Subject objects as I explained on my other article.
Therefore, just for brevity, I will include the code here as a quick reference.
const Subscription = function(handlerId, unsubscribeNotificationCallback) {
const self = this;
self.unsubscribe = () => {
if (unsubscribeNotificationCallback) {
unsubscribeNotificationCallback(handlerId);
}
};
return self;
};
const Subject = function(subscribersStateChangeNotificationCallback) {
const 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);
}
}
};
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);
});
};
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;
};
Authentication Service
const AuthenticationService = function() {
const self = this;
self.loggedInUser = new Subject();
self.logIn = function(username, password) {
// log in, get some user details (username, age,....), and update loggedInUser
let user = {
username: username,
age: age
};
self.loggedInUser.next(user);
};
return self;
};
This is what we can notice here:
Inside the AuthenticationService, we have a loggedInUser which is a Subject.
Using this loggedInUser, other modules can subscribe to the stream of changes applied on the logged in user.
We also have logIn function which does some API calls and finally sets the logged in user and trigger the loggedInUser Subject.
Initializing the Authentication Service
Somewhere in the main application, it is as simple as that.
const authenticationService = new AuthenticationService();
Some Module
const SomeModule = function() {
const self = this;
let loggedInUser = null;
const subscription = authenticationService.loggedInUser.subscribe((user) => {
loggedInUser = user;
});
self.DoSomeStuff = function() {
// need to know the logged in user
console.log(self.loggedInUser);
};
console.log(self.loggedInUser); // self.loggedInUser would be null
return self;
};
This is what we can notice here:
The module subscribes to the authenticationService.loggedInUser Subject to get updates about the logged in user.
Whenever an update happens, a local variable called loggedInUser would be set.
On the DoSomeStuff function, we log the value of the local variable loggedInUser which should be in sync with the latest updates on the AuthenticationService… or not?
Actually, not. The problem is that by the time we are creating an instance of the SomeModule, the AuthenticationService would already be created and initialized.
This means that on line number 15 in the code above, the local loggedInUser variable would still be null because up to that moment it was never set to another value.
The Right Way to Do It
It is not that complicated actually. There is a pattern used in this kind of cases and I am sure you had seen it before. It is even applied in different client-side Frameworks.
Enough talking, let’s see some code.
Enhanced Authentication Service
const AuthenticationService = function() {
const self = this;
self.snapshot = {
loggedInUser: null
};
self.loggedInUser = new Subject();
self.logIn = function(username, password) {
// log in, get some user details (username, age,....), and update loggedInUser
self.snapshot = {
loggedInUser: {
username: username,
age: age
}
};
self.loggedInUser.next(self.snapshot.loggedInUser);
};
return self;
};
This is what we can notice here:
We added a new member called snapshot and it represents an object with a logged in user as a member inside.
We also updated the logIn function implementation in a way that it first updates the snapshot object and then does what it used to do.
Enhanced Some Module
const SomeModule = function() {
const self = this;
let loggedInUser = authenticationService.snapshot.loggedInUser;
const subscription = authenticationService.loggedInUser.subscribe((user) => {
loggedInUser = user;
});
self.DoSomeStuff = function() {
// need to know the logged in user
console.log(self.loggedInUser);
};
console.log(self.loggedInUser); // self.loggedInUser would have some value
return self;
};
This is what we can notice here:
We updated the initialization of the local loggedInUser variable to get its value from the authenticationService.snapshot.loggedInUser.
Therefore, on line 15 in the code above, the value of the local loggedInUser variable would not be null, it would hold the latest value of the logged in user.
Finally, hope you found reading this story as interesting as I found writing it.
Comments