Understanding NgZone
Angular 2 does a lot of things differently from Angular 1, and one of its greatest changes is in change detection.
Join the DZone community and get the full member experience.
Join For FreeAngular 2 does a lot of things differently from Angular 1, and one of its greatest changes is in change detection. understanding how it works has been essential, particularly when using Protractor for E2E testing. This explores how to work with zones for testing and performance. A live example of the mentioned code is here.
The biggest change in how Angular 2 handles change detection, as far as a user is concerned, is that it now happens transparently through zone.js.
This is very different from Angular 1, where you have to specifically tell it to synchronize – even though both the built-in services and the template bindings do this internally. What this means is, while $http or $timeout do trigger change detection, if you use a third party script, your Angular 1 app won’t know anything happened until you call $apply().
Angular 2, on the other hand, does this entirely implicitly – all code run within the app’s Components, Services or Pipes exists inside that app’s zone, and just works.
So, What Is a Zone?
zone.js’s zones are actually a pretty complicated concept to get our head around. Running the risk of over-simplifying, they can be described simply as managed code calling contexts – closed environments that let you monitor, control, and react to all events, from asynchronous tasks to errors thrown.
The reason this works is, inside these zones, zone.js overrides and augments the native methods – Promises, timeouts, and so on, meaning your code doesn’t need to know about zone to be monitored by it. Everytime you call setTimeout, for instance, you unwillingly call an augmented version, which zone uses to keep tabs on things.
What Angular does is use zone.js to create it’s own zone and listen to it, and what this means for us as angular users is this – all code run inside an angularapp is automatically listened to, with no work on our part.
Most times, this works just fine – all change detection “just works” and Protractor will wait for any asynchronous code you might have. But what if you don’t want it to? There are a few cases where you might want to tell angular not to wait for / listen to some tasks:
- An interval to loop an animation
- A long-polling http request / socket to receive regular updates from a backend
- A header component that listens to changes in the Router and updates accordingly
These are cases where you don’t want angular to wait on asynchronous tasks/ change detection to run every time they run.
Control Where Your Code Runs With NgZone
NgZone gives you back control of your code’s execution. There are two relevant methods in NgZone – run and runOutsideAngular:
- runOutsideAngular runs a given function outside the angular zone, meaning its code won’t trigger change detection.
- run runs a given function inside the angular zone. It is meant to be run inside a block created by runOutsideAngular, to jump back in, and tell angular to start listening again.
So, this code will have problems being tested, as the app will be constantly unstable:
this._sub = Observable.timer(1000, 1000)
.subscribe(i => {
this.content = "Loaded! " + i;
});
Using NgZone, we can avoid that problem:
this.ngZone.runOutsideAngular(() => {
this._sub = Observable.timer(1000, 1000)
.subscribe(i => this.ngZone.run(() => {
this.content = "Loaded! " + i;
}));
});
Simplifying usage
After understanding how NgZone works, we can simplify its usage, so that we don’t need to sprinkle NgZone.runOutsideAngular and NgZone.run all over the place.
We can create a NgSafeZone service to do exactly that, as the most common use case for this is:
- Subscribe to an Observable outside the angular zone
- Return to the angular zone when reacting to that Observable.
@Injectable()
export class SafeNgZone{
constructor(private ngZone: NgZone) {
}
safeSubscribe(
observable: Observable<T>,
observerOrNext?: PartialObserver<T> | ((value: T) => void),
error?: (error: any) => void,
complete?: () => void) {
return this.ngZone.runOutsideAngular(() =>
return observable.subscribe(
this.callbackSubscriber(observerOrNext),
error,
complete));
}
private callbackSubscriber (obs: PartialObserver<T> |
((value: T) => void)) {
if (typeof obs === "object") {
let observer: PartialObserver<T> = {
next: (value: T) => {
obs['next'] &&
this.ngZone.run(() => obs['next'](value));
},
error: (err: any) => {
obs['error'] &&
this.ngZone.run(() => obs['error'](value));
},
complete: () => {
obs['complete'] &&
this.ngZone.run(() => obs['complete'](value));
}
};
return observer;
}
else if (typeof obs === "function") {
return (value: T) => {
this.ngZone.run(() => obs(value));
}
}
}
}
With this the previous code gets simplified quite a bit:
// The following:
this.ngZone.runOutsideAngular(() => {
this._sub = Observable.timer(1000, 1000)
.subscribe(i => this.ngZone.run(() => {
this.content = "Loaded! " + i;
}));
});
// Becomes:
this._sub = this.safeNgZone.safeSubscribe(
Observable.timer(1000, 1000),
i => this.content = "Loaded! " + i);
Published at DZone with permission of Tiago Roldao, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments