A short guide to understand ZoneJs and to solve a problem.
How about no upfront elucidation of the topic but a story. Okay here it goes - it was a regular morning at work. I as usual was sipping my coffee and trying to think of solution to a problem. The problem at hand was to implement a progress bar which tracks all the API calls before landing onto the actual page (loaded with the API data, Obviously!). As the application deals with myriads of data, this loader was to be designed to track lot of API calls. "How hard can it be?" , I thought. But the problem which seemed puny at the beginning, later turned out to be a nightmare.
Initially...
I was almost a novice in understanding how to use Angular's powerful tools to develop this. So,like any other problem I started looking out for possible ways to implement this in Angular. I read many blogs and came across several Stack Overflow posts. Everything I found was pretty much useless. None of them seemed efficient in the current context. There are no modules or libraries that accomplishes this. I started feeling worried. But I came up with a solution that made sense in my head and I was excited again.
Sometimes the simplest solution is the best, but sometimes it's not
The solution was straight forward. Create a progress-bar component and use a service to trigger the bar to move forward. Simple enough!
I started off by creating model class for the message :
export class Message {
id: string;
message: string;
active: boolean;
}
After the model is ready, next I created the progress loader component i.e. ProgressBarComponent :
// other imports
import { Message } from '../../models/interfaces';
@Component({
selector: 'progress-bar',
templateUrl: './progress.bar.component.html',
styleUrls: ['./progress.bar.component.scss']
})
export class ProgressBarComponent implements OnChanges {
@Input() messages: Message[] = [];
@Output() loadingEmitter = new EventEmitter<boolean>();
constructor() { }
public activeMessage: Message = { id: '', message: '', active: false };
public progressCount = 0;
ngOnChanges() {
/* Code to check progress count and set the active message on the loader */
/* Actual code removed for the sake of brevity. */
}
}
And the service to trigger the active message i.e. :
// other imports
import { Message } from '../../../../models/interfaces';
@Injectable({
providedIn: 'root'
})
export class LoadTrackerService {
constructor() {}
public loaderMessages: Message[] = [
{ id : 'm_id_1', message: 'Load Started,API 1 called', active: true },
{ id : 'm_id_2', message: 'API 2 called', active: false },
{ id : 'm_id_3', message: 'API 3 called', active: false },
{ id : 'm_id_4', message: 'API 4 called', active: false }
{ id : 'm_id_5', message: 'API 5 called, Load Complete', active: false }
];
public loadingPercent: number;
public loading = true;
public messageSubject = new BehaviorSubject<Message[]>(this.loaderMessages);
setMessage(messageId: string) {
if (this.activateMessage(messageId)) {
this.messageSubject.next(this.loaderMessages);
}
}
activateMessage(messageId: string): Boolean {
/* Code to activate message on the loader and return boolean on
activation*/
/* Actual code removed for the sake of brevity. */
}
}
Now the messageSubject will be triggered by the APIService(service where all API calls are made) and is subscribed by the loader component to retrieve the active message and increment the loader. Everything made sense until I realized the real issue.
Off to a bad start
I soon realized that there was no way to track the API calls, all I could do is trigger the LoadTrackerService inside each method of APIService as below :
@Injectable({
providedIn: 'root'
})
export class APIService {
constructor(
private loadTracker: LoadTrackerService) {}
api_call_1() {
/* Http call for API 1*/
this.loadTracker.setMessage('m_id_1');
}
api_call_2() {
/* Http call for API 2*/
this.loadTracker.setMessage('m_id_2');
}
api_call_3() {
/* Http call for API 3*/
this.loadTracker.setMessage('m_id_3');
}
// and so on...
}
Now this above solution can certainly be applied where there are not many API calls, but in an actual real world scenario with 100s of API calls, this approach would make the code dirty and repetitive. I needed something better and cleaner.
Get to the safe zone(js) ;)
So after a lot of researching and reading various in-depth articles on Angular, I came across this article. Turns out Angular handles and tracks all the API calls inside something called a Zone. Zones is the mechanism to handle logically connected async calls. Angular (or rather ZoneJS) conveniently calls them microtasks . It became very clear now, how to take advantage of this magic.
I started by creating a new Zone by forking the angular default zone and called it trackerZone. It is very important to place this Zone logic inside a resolver(Angular route resolver), so to trigger it and get it resolved before we land onto the actual page.
import { Injectable, NgZone } from '@angular/core';
// other imports...
@Injectable()
export class ProjectResolver implements Resolve<any> {
constructor(
private ngZone: NgZone,
private loadTracker: LoadTrackerService,
) { }
public trackerZone: NgZone;
resolve() {
return this.resolveInTrackerZone();
}
resolveInTrackerZone() {
this.trackerZone = this.ngZone['_inner'].fork({
properties: {
countSchedule: 0,
loaderRef: this.loadTracker
},
onScheduleTask(delegate, currentZone, targetZone, task)
{}
});
Let me explain quickly what's happening here. For accessing the default Angular Zone, we can import it from 'angular/core'. So I have instantiated it into a private variable called ngZone, so that we use the zone reference for forking later. Next I have created my very own trackerZone .
Now we can fork the zone instance and assign it to our trackerZone.
Now we can pass properties / values / references to the trackerZone inside the properties object. Along with that we get a onScheduleTask callback method, which gets fired every time any task fires. The thing worth mentioning here is that, apart from microtasks there are different types of tasks, that we won't discuss here but are as also important. If you want to understand it better, I highly recommend this blog. The task is an object with various properties like type, data etc. (used below)
The next thing to do was to run all the API calls inside the tracker zone by using trackerZone.run() method. That's all you have to do, to get Angular fire the Zone magic and give us microtasks.
/
import { Injectable, NgZone } from '@angular/core';
// other imports...
@Injectable()
export class ProjectResolver implements Resolve<any> {
constructor(
private ngZone: NgZone,
private loadTracker: LoadTrackerService,
) { }
public trackerZone: NgZone;
resolve() {
return this.resolveInTrackerZone();
}
resolveInTrackerZone() {
this.trackerZone = this.ngZone['_inner'].fork({
properties: {
countSchedule: 0,
loaderRef: this.loadTracker
},
onScheduleTask(delegate, currentZone, targetZone, task)
{
const result = delegate.scheduleTask(targetZone,
task);
const url = task['data']['url'] || '';
const tracker = this.properties.loaderRef;
if (task.type === 'macroTask' && task._state !==
'unknown') {
/* Triggering the message service based on URL */
}
return result;
}
}
});
this.trackerZone.run(() => {
/* return Observable / API call / Parallel Calls*/
});
}
}
Here we scheduled the tasks manually inside our trackerZone using delegate.scheduleTask(targetZone, task) . Now we just have to map the URLs with the message ids and trigger the service.
if (task.type === 'macroTask' && task._state !== 'unknown') {
this.properties.countSchedule += 1;
if (url.indexOf('id_1') > -1) {
tracker.setMessage('m_id_1');
} else if (url.indexOf('id_2') > -1) {
tracker.setMessage('m_id_2');
} else if (url.indexOf('id_3') > -1) {
tracker.setMessage('id_3');
}
// and so on...
}
That's all there is ! I really loved how easy and convenient ZoneJS makes this whole process. Just to cover all the bases, another way of doing this could be using HTTP interceptors, but I feel ZoneJS is much more elegant and intuitive. Again it's just my opinion.
Lastly, this is my first blog post ever. Don't hate me for being a noob at writing, I will get better. Please drop some kind words in the comments below, if you like it.
Peace π
Top comments (26)
Good article explaining a use case of ZoneJS.
I wouldn't recommend using zone for such a task! Interceptors are better suited from readability and maintainability aspects.
I ran into many situations where the developer doesn't know a thing about ZoneJS which is actually fine, ZoneJS is rarely used. also, you had to use private property
ngZone['_inner']
andtask._state
to make it.Moreover, this part
task.type === 'macroTask'
, way much dealing with inner work of javascript.Peace π
Hey @ezzabuzaid , Thanks for your comment. I should have really explained the situation better here.
So I tried this with interceptors and opted the ZoneJs solution because of the following points :
Making ngZone['_inner'] and task._state private was a helpful suggestion. I would definitely keep that in mind.
Up for a further discussion on this. βοΈ
He's not telling you to "make ngZone['_inner'] and task._state private", since I don't think you're a ZoneJS mantainer.
He's telling you that you are accessing private ZoneJS properties , and sorry but this is just plain wrong.
You are using undocumented features you probably copied from somewhere, and, even worse, you are advising others to use them.
This is always wrong (in any programming language) for at least two reasons:
1) You can't really know what's going on here. Unless you can know why exactly you are forking the _inner zone in that exact moment and how it is exactly handled itnernally, this would be random hocus pocus to you, that somehow does its purpose. If you can't understand how something works exactly, you will have a hard time debugging it.
2) If something is private and undocumented, it's just not supposed to be used from the general public. Thus, it can be subject to hard changes at any time, and without any notice.
I.E: maybe one day task objects might no longer have a "data" property, so task['data']['url'] will fail.
This doesn't mean the advice would be "always check if tasks have a data property". This means "don't use the undocumented data property".
Also, I didn't try this but in Angular trying to access private properties might throw hard exceptions when compiling AOT.
You can write a correct solution using an Interceptor, which is pretty standard and documented, and way more clean.
If you define all your messages in a Model, and use your Component correctly, you can do everything with a Service, a Model, a Component and an Interceptor, and a lot less custom code.
The Interceptor will read the correct message from the Model, call a method on the Service that will publish it, and update the Component. If you need more data to be shared with the Component, you can do it on the Service.
More on this, having message "ids" and then matching them with routes in long custom if chains looks like a lot of overhead.
You would most probably rather have a route attribute in your messages (which could be a regex) and match with it.
Honestly, I don't want to be too harsh or judgemental, but even your basic solution doesn't seem to follow Angular best practices.
So it's nice of you to want to help people with your tutorials, but you would make people a bad disservice if you point them the wrong ways.
Your Component gets an Input "messages" object (and for some reason an Output "loadingEmitter" whose purpose you never explain), and your Service has a "messageSubject". But you never show how one acts on the other.
Worse, republishing this.loaderMessages on the messageSubject is just plain wrong.
Javascript objects are always passed by reference, so whatever has access to the published messages object will always have access to the current version of it (with the updated active flags). There's no need to republish it.
You are probably doing it to trigger Change Detection, but even this doesn't add up. Unless you are doing some magic somewhere outside of the Component to force it, the component's ngOnChanges should never be fired, since the Input property is always a reference to the same object, republished but not changed.
That is, unless you are subscribing to the messageSubject (which you don't show) and cloning the object before passing it to the Component, or something like that.
Using an Observable is correct, but the Component itself should subscribe to that Observable, and update the template accordingly. There's no need to force an Input property into it.
Also, that Observable should probably emit the current active message. This way you can easily have proper Change Detection.
Hey Ivan , Thanks for your kind comment. Let me calmly clarify you certain things.
First of all, thanks a lot for taking time to drop a detailed comment, I really appreciate it. Let me talk about everything one by one.
I missed the meaning of private property part, I thought what he meant was to keep these properties private to your zone, I apologise for that.
The part about copying stuff from somewhere, I am pretty sure you copy stuff from articles / blogs too, as I don't think you are on the development team of Angular or something significant for that matter. (I gave the blog link for everyone to clarify their doubts). Also I said I solved the problem like this, and did not ADVICE anyone to use it, as far as I know there are no single solutions in engineering. You can chose to ignore the solutions you don't like.
Okay I might know what's going on here as I am still learning and also writing my first blog here, but I think you you're so intimidated because you definitely don't know anything what's going on here. I have read 10+ articles on ZoneJS, watched videos and read documentation, and I can certainly explain what is "forking the _inner zone" , we can discuss this off the comment section.
Private and undocumented things are not supposed to be used ??? Then I am sure you're well versed with ZoneJS documentation, please direct me to the place where this is mentioned that "DON'T use ZoneJS , it's a hidden feature made for internal use". ZONEJS is not a hack, just to clarify. It's available at 'angular/core'. I don't think I need to explain core to you.
Your solution with interceptors , let me give you a scenario. There are two arrays (A & B)of parallel API calls merged into an observable using lets say "forkJoin" in the same service. Both the arrays have one url which is same, I want to track the url from A and not from B, can interceptor handle that. Instead of suggesting solutions for how to make the message service work, try to fix this problem, which I am sure you did not get properly.
May be the ids solution is an overhead, I am sure I can do better there. You're right there.
About the component accepting messages array, loaderEmitter etc and whose purpose I did not mention. Again if you understood the point of the post you would not asked this question. I have written "/* Actual code removed for the sake of brevity. */" . That means something, that means the code is not important for this blog's perspective.
Okay lastly, broadcasting this.loaderMessages on the messageSubject is just plain wrong . I am so puzzled at this, obviously I know JS objects are passed by reference, and in fact I am using change detection strategy- onPush. Quick Angular tip for you, whenever you do that you use ngDoCheck not ngOnChanges. Also, the purpose of broadcasting the whole array was to do some calculation on all the subscribers as soon as it happens, as I haven't implemented ngDoCheck.
Again these are the things, which were just there to give you a glimpse of the working of the loader, which could have been implemented in 100 different ways. Commenting on that is just digressing from the topic.
I am so baffled that how people with no background information about the application, can judge and pass comments about the code. Also it's not an advice to follow this approach to achieve it, it's just me playing around with the code.
Hope this helps you understand things a little better and being little less hateful with your comments.
PEACE !
Hi Suvendu,
I clearly failed in not making my comment sound judgemental.
As much as you say you thank me and appreciate it, the sound of your answer ("I don't know if you can read English properly", "I only do educated debates", "before furiously tapping your keyboard") clearly shows you found it offensive.
I didn't mean that and I'm sincerely sorry if you found it "hateful".
You say you are not advising people to use your solution, but your post is literally titled "A short guide to understand ZoneJs and to solve a problem".
If your solution involves accessing private properties of Angular internals, it's a wrong solution.
You are free to think otherwise, but you are never supposed to access private properties. Of course you won't find any documentation saying you're not supposed to, since it's already taken for granted.
Even if you don't believe it's just wrong, you can find two valid pratical reasons not to do it in my previous comment.
And I really wonder if your code would compile in production.
You are right in saying I don't know the specifics of your project, but I don't agree that should hold me from commenting on that.
If you want to write a guide that says using undocumented features is better then using documented and standard ones, you should point out in which exact scenario this is true. If you hide it, it will sound like overcomplicating.
To answer your question about the Interceptor, I can't be more precise since as you say I don't know the details, but an idea could be adding params to the http query instructing the Interceptor on how to handle it. Or maybe using the Service to handle communication between what is running the query, the Interceptor and/or the Component.
Lastly, re-emitting the same object is always a bad practice. You are showing this in your code, then you're showing a bad practice. You might be doing something that you hide to force change detection, and I don't know what exactly, but that's because you are using a bad practice.
I don't want to force this on you, so you can take it as just a matter of personal preference.
Again, sorry if you found my comments offensive, I really didn't mean it.
For what it's worth, I sincerely like your writing and find it engaging, and that's why I read the whole article even if I was puzzled at the code.
If I criticized your code is because I like your style, and thought it would be better if you sticked on writing about documented code and good practices.
If I didn't like anything about your post, I sure would have ignored it, I didn't mean to write a destructive comment.
Hi Ivan ,
Sorry for lashing out , I edited my above comment with better phrases which do not sound offensive. I am certainly here to learn from everyone in the comment section . It's very hard not to take offense on the phrases you used like (You don't really know what's going on here, hocus pocus , etc). Could have easily been rephrased in a better way. In sounds to be destructive that way.
The title of the blog ("A short guide to understand ZoneJs and to solve a problem.") is subject to interpretations, I myself never just read one blog and implement anything. So it depends how you interpret it.
My code is compiled and deployed at at least 10 clients locations, including some big IT giants like Biocon, and even at organizations like ISRO etc. I don't want you to take it the wrong way, but please read a bit about ZoneJS, it's not much mysterious and cryptic. I can even refer you some good blogs. Because most of the things you say are wrong are just your opinions. Again, I mean no offense.
Also everything we discussing is kinda debatable except broadcasting an array, that can be done in a better way. The only thing I beg to differ is your interceptor approach in the current context. We can connect outside this comment section and discuss that if you like.
Sorry again for using those phrases, i should have handled it better.
Thanks , Cheers ! :)
Hi Suvendu,
thank you for accepting my goodwill and softening the tones.
As you edited your comment, I also edited my first one to clarify parts that indeed, now that you highlight them, could sound offensive.
I know that there are guides about ZoneJS, and you sure read a lot more than me about it, so you sure have a better picture than me. But those guides are not official, the official documentation is pretty scanty about it.
When I wrote "you don't really know what's going on" I didn't mean you don't know because of your lack of knowledge, I mean you (or me) don't know because there's no way to know (I now edited that part).
Even if people "reverse engineered" it and wrote guides, do they really know exactly when and how the _inner zone gets updated? Why should we fork the _inner one and not the _outer one? Has it always been consistent in all Angular versions? Why aren't we using the forkInnerZoneWithAngularBehavior() method instead? What does AngularBehavior even mean?
Of course I do copy code on an almost daily basis, but if I copy something from an unofficial source, something which is undocumented but only explained by the author to the best of his knowledge, I admit it indeed is hocus pocus. With which I mean something that just works that way because it does, and it's not guaranteed to work perfectly and forever to my needs. And if I wanted to change something about it I wouldn't know where exactly to look other than unofficial guides by people who write to the best of their knowledge.
I'm not saying it doesn't happen to me, but it's not a good practice, so if there's a documented and more conventional alternative I prefer it.
Also, given the rate that Angular introduces breaking changes in documented features, I wouldn't be surprised at all if they introduced them in an undocumented one (which as far as they know is only used in Angular itself). And of course the breaking changes would be undocumented themselves.
I believe you when you say you do use that code in production, but it honestly sounds weird that it doesn't throw compiling exceptions when accessing private properties, at least with Ivy.
Maybe I will try it out of curiosity when I get back to work (now I am on holiday, though it doesn't look like so;)).
Thank you again for helping me not escalating the argument, and keep on with your blogging. As I already wrote I like your style, it sure doesn't look like someone's first post in any negative way!
If you ask me why I don't write my own blog instead than criticizing your code, the answer is that I would do a terrible job, so it's better to leave it to you.
Maybe with more constructive and well worded comments from people:)
Hey Ivan ,
Thanks for taking high grounds man ! Let's do our bits to develop the community. Me with my poorly chosen codes XD and you with your constructive comments. :D
I will surely come up with a better / well researched blog on ZoneJS in future and definitely tag you there.
Again, thanks for all the compliments about my writing, I really appreciate it.
Cheers :)
Good one., but i would say over engineered. I think interceptor is the way to go. Im not a big fan of zone because I have to mostly go with zoneless approach. Mostly when working with micro frontend / micro apps. Just my opinion tho.
Hey Rameez, Thanks for your comment. :)
I should have done a better job at elaborating the situation as most of the comments are making this point. I tried to explain why I did not go for interceptors in other comments. Please go through them and see if that makes sense.
Over engineered in my opinion. I'd just use, services, events or observables.
Hey John Peters,
Thanks for your comment. In my opinion, the only other option here would be to use HTTP interceptors to achieve this. I couldn't find any other way of doing this with services , events or observables. :(
Please direct me to any blog / post / article to achieve it , if I am missing something here.
Thanks . βοΈ
If the client is firing the async requests they can track the response right? Be it just one or hundreds all promises can be combined and monitored for completion. As each one completes a message is sent to the tracker. Only 2 things are known, the promise and the resolve.
That's just one extra line of code inside each subscribe or then (100 repetitive lines of code for 100 responses , and I haven't even counted calls which are merged into one observable, what if I want to track each one seperately, like I want to show a different message for each call ), defeats the purpose of the whole post. Sorry but I don't see a solution here .
All work can be refactored ro just one function which sends the request, subscribes and posts a completion message.
Really interesting, the examples could be more realistic if I had to say something π
It's sure now, after many readings I have to Integrate in the conception the NgZone's dimension !
Thx for this great post, keep going, peace π
Hey @azword , Thanks for your comment , it means a lot.
I would work on my examples better next time.
Thanks βοΈ
It looks like anything but that actually π
Hey @fyodorio , Thanks for your comment. βΊοΈβοΈ. I meant that part for, like once you setup your zone and you can run desired set of calls inside your zone, with almost not bothered about the rest of application and not tracking all the other calls you don't want to track.
Although yeah setting up the zone is no piece of cake. π
Great post to understand zone.
Could you elaborate a little more why an interceptor to call the loader service before and after the request wasn't enough?
Hey Sebastian, thanks a lot for your comment . I am really glad you liked it.
To answer your question, I have mentioned the problems with interceptor in the comments above. Let me just copy paste that here for your ease of understanding.
**** Copied from above ****
So I tried this with interceptors and opted the ZoneJs solution because of the following points :
Hope it helps . :)
Angular is going to make it optional
Hey @rahiyansafz , yeah I read a bit about it, you can tree shake by adding noop in zone .
Great post, never knew this about Angular. Thanks for sharing!
@treblecode made my day :)
ezza has agood point