From zone.js to zoneless Angular and back — how it all works
Tags: angular
Published: 2023-01-25 16:09:16

---
title: From zone.js to zoneless Angular and back — how it all works
published: true
description: 
tags: angular
canonical_url: https://indepth.dev/posts/1513/from-zone-js-to-zoneless-angular-and-back-how-it-all-works
# cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b2ozzvn5ep3q83ucc0nf.png
# Use a ratio of 100:42 for best results.
# published_at: 2023-01-25 14:50 +0000
---

> This article is an excerpt from my [Angular Deep Dive](https://courses.indepth.dev/angular?utm_source=devto&utm_medium=article&utm_campaign=autorun-with-zones) course

# Autorun with zones

Change detection (rendering) in Angular is usually triggered completely automatically as a result of async events in a browser. This is made possible by utilizing zones implemented by [zone.js](https://github.com/angular/angular/blob/297d865e9e1ca0ffa183988c16429f16bc2ccbd7/packages/zone.js) library. In general, zones provide a mechanism to intercept the scheduling and calling of asynchronous operations. Interceptor logic can execute additional code before or after the task and notify interesting parties about the event. These rules are defined individually for each zone when it’s being created.

Zones are composed in a hierarchical parent-child relationship. At the start the browser runs in a special root zone, which is configured to behave exactly like the platform, making any existing code which is not zone-aware behave as expected. Only one zone can be active at any given time, and this zone can be retrieved through `Zone.current` property:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m3meztrl70mzblwm7iv8.png)

If you’re interested to learn how to work with zone.js directly, check out [this article](https://indepth.dev/posts/1135/i-reverse-engineered-zones-zone-js-and-here-is-what-ive-found).

**Contrary to popular belief, zones are not part of the change detection mechanism in Angular.** In fact, Angular can work without zones using [change detection services](/angular/running-change-detection/manual-control). To enable automatic change detection, Angular implements [NgZone](https://github.com/angular/angular/blob/30d5a2ca83c9cf44f602462597a58547b05b75dd/packages/core/src/zone/ng_zone.ts#L86) service that forks a child zone and subscribes to notifications from this zone.

This zone is referred to as Angular zone and all application specific code is expected to run inside this zone. That’s because `NgZone` only gets notifications about events that occur inside this Angular zone and doesn’t get any notifications about events in other zones:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ps0ws7gq9z0ef5vgirak.png)

If you explore `NgZone`, you’ll see that the reference to the forked Angular zone is [stored](https://github.com/angular/angular/blob/86372538ab017058cb9b2a7910e761d823bd3c16/packages/core/src/zone/ng_zone.ts#L400) in the `_inner` property:

```ts
export class NgZone {
  constructor(...) {
    forkInnerZoneWithAngularBehavior(self);
  }
}

function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
  zone._inner = zone._inner.fork({ ... });
}
```

This is the zone that is used to run a callback when you execute `NgZone.run()`:

```ts
export class NgZone {
	run(fn, applyThis, applyArgs) {
	    return this._inner.run(fn, applyThis, applyArgs);
	}
}
```

The current zone at the moment of forking the Angular zone is kept in the `_outer` property and is used to run a callback when you execute `NgZone.runOutsideAngular()`:

```ts
export class NgZone {
	runOutsideAngular(fn) {
	  return (this as any as NgZonePrivate)._outer.run(fn);
	}
}
```

Most often this outer zone is the top-most “root” zone.

When Angular finished its bootstrapping, that’s the hierarchy of zones you get:

```tsx
"root"
   "angular"
```

Which you can easily see for yourself by simply logging the corresponding properties:

```ts
export class AppComponent {
  constructor(zone: NgZone) {
    console.log((zone as any)._inner.name); // angular
    console.log((zone as any)._outer.name); // root
  }
}
```

However, in the development mode, there’s also `AsyncStackTaggingZone` that sits in between `root` and `angular` zone like this:

```ts
"root"
   "AsyncStackTaggingZone"
		       "angular"
```

In this case, `NgZone` instance keeps a reference to the `AsyncStackTaggingZone` in its `_inner` property. `AsyncStackTaggingZone` provides linked stack traces to show where the async operation is scheduled. For more details, refer to the article [Better Angular Debugging with DevTools](https://developer.chrome.com/blog/devtools-better-angular-debugging/).

The last bit that we need to know is that Angular instantiates `NgZone` during the bootstrapping phase:

```ts
export class PlatformRef {
		bootstrapModuleFactory<M>(moduleFactory, options?) {
        const ngZone = getNgZone(options?.ngZone, getNgZoneOptions(options));
        const providers: StaticProvider[] = [{provide: NgZone, useValue: ngZone}];
        // all initialization logic runs inside Angular zone
        return ngZone.run(() => {...});
		}
}
```

Angular uses the [onMicrotaskEmpty](https://github.com/angular/angular/blob/ff84c7360334792a9a422bd0fa0353e0cf670525/packages/core/src/application_ref.ts#L766) event inside `ApplicationRef` to automatically trigger change detection for the entire application:

```ts
@Injectable({providedIn: 'root'})
export class ApplicationRef {
  constructor(
      private _zone: NgZone,
      private _injector: EnvironmentInjector,
      private _exceptionHandler: ErrorHandler,
  ) {
    this._onMicrotaskEmptySubscription = this._zone.onMicrotaskEmpty.subscribe({
      next: () => {
        this._zone.run(() => {
          this.tick();
        });
      }
    });
  }
}
```

Let’s now see how Angular can work without zones.

## Zoneless application

To run an Angular application without `zone.js` we need to pass `noop` value into `bootstrapModule` function for `ngZone` parameter like this:

```ts
platformBrowserDynamic()
    .bootstrapModule(AppModule, {
        ngZone: 'noop'
    });
```

If we now run this simple application:

```ts
@Component({
  selector: 'app-root',
  template: `{{time}}`
})
export class AppComponent {
  time = Date.now();
}
```

we’ll see that change detection is fully operational and renders `time` value in the DOM.

However, if update the `name` property inside the callback for `setTimeout`:

```ts
@Component({
  selector: 'app-root',
  template: `{{time}}`
})
export class AppComponent {
  time = Date.now();

  constructor() {
    setTimeout(() => { this.time = Date.now() }, 1000);
  }
}
```

We’ll see that the change is not updated. This is expected behavior since there’s no Angular zone to notify Angular about the timeout event occurrence. What’s interesting that we can still inject `NgZone` into the constructor:

```ts
import { ɵNoopNgZone } from '@angular/core';

export class AppComponent {
  constructor(zone: NgZone) {
    console.log(zone instanceof ɵNoopNgZone); // true
  }
}
```

But it’s an empty implementation of `NgZone` which does nothing. We could use a change detector service to manually run change detection:

```ts
@Component({
  selector: 'app-root',
  template: `{{time}}`
})
export class AppComponent {
  time = Date.now();

  constructor(cdRef: ChangeDetectorRef) {
    setTimeout(() => { this.time = Date.now() }, 1000);
    cdRef.detectChanges();
  }
}
```

We’ll go over this service in detail in
[manual control](/angular/running-change-detection/manual-control) chapter.

## Running code inside Angular zone

You may find yourself in the situation where you have a function that somehow runs outside of Angular's zone and you don’t get the beneif to automatic change detection. It’s a common scenario with a third party library doing its stuff unaware of Angular context.

Here is [an example of such question](https://stackoverflow.com/a/46286400/2545680) involving [Google API Client Library](https://developers.google.com/api-client-library/) (gapi). The common culprit is using techniques like JSONP that don’t use common AJAX APIs like
[XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) or [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) which are patched and tracked by Zones. Instead, it creates a `script` tag with a source URL and defines a global callback to be triggered when the requested script with data is fetched from the server. This can’t be patched or detected by Zones and hence the frameworks remains oblivious to requests performed using this technique.

The common solution to such problems is to simply run a callback inside Angular zone like this. For example, for [gapi](https://github.com/google/google-api-javascript-client/blob/master/docs/start.md#option-1-load-the-api-discovery-document-then-assemble-the-request) we should do it like this:

```js
// Load the JavaScript client library.
gapi.load('client', ()=> {
    // Run initialization code INSIDE Angular zone
    NgZone.run(()=>{
        // Initialize the JavaScript client library
        gapi.client.init({...}).then(function() { ... });
    });
});
```

Another example is a component that emits notifications from a callback that runs outside the Angular zone:

```ts
@Component({
  selector: 'n-cmp',
  template: '{{title}} <div><n1-cmp></n1-cmp></div>'
})
export class N {
  title = 'N component';
  emitter = new Subject();

  constructor(zone: NgZone) {
    zone.runOutsideAngular(() => {
      setTimeout(() => {
        this.emitter.next(3);
      }, 1000);
    });
  }
}
```

If we simply subscribe to the changes in the child `N1` component like this:

```js
@Component({
  selector: 'n1-cmp',
  template: '{{title}}, emitted value: {{value}}'
})
export class N1 {
  title = 'Child of N';
  value = 'nothing yet';

  constructor(parent: N, zone: NgZone) {
    parent.emitter.subscribe((v: any) => {
      this.value = v;
    });
  }
}
```

we won’t see any updates on the screen, even though the `this.value` is updated to `3` after the parent emits it.
To fix this, just like in the `gapi` example, we could run the callback in Angular zone:

```js
@Component({...})
export class N1 {
  title = 'Child of N';
  value = 'nothing yet';

  constructor(parent: N, zone: NgZone) {
    parent.emitter.subscribe((v: any) => {
      zone.run(() => {
        this.value = v;
      });
    });
  }
}
```

this fixes it.