DEV Community

Cover image for Zoneless Change Detection in Angular 18
OpenReplay Tech Blog
OpenReplay Tech Blog

Posted on

Zoneless Change Detection in Angular 18

by Ugochukwu Sabastine

Change detection is important in Angular, ensuring that the UI shows the application's current state. Traditionally, Angular uses Zone.js

To automatically detect and apply these changes to the UI, but with the Angular 18 release, a new approach was introduced—zoneless change detection, as this article will show.

Session Replay for Developers

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — an open-source session replay suite for developers. It can be self-hosted in minutes, giving you complete control over your customer data.

OpenReplay

Happy debugging! Try using OpenReplay today.



Change detection in Angular involves tracking and updating changes in data components to the UI. It ensures that interaction is displayed in real-time when we interact with an Angular app. This article will take us through change detection, how Zone.js works, and the reason for the shift to applications without zones. We will also see how to set up a change detection process in a Zoneless application.

Zone.js works by monkey-patching all async APIs and creating a zone for each async task. It will keep track of operations from their beginning to their end. The change detection mechanism is triggered when the task is done, and the UI is updated.

It's important to note that Angular will not discontinue Zone.js since many apps have already been made using it. They will continue to support Zone.js by making critical fixes and security patches.

Why is Angular moving towards Zoneless?

As applications scale, the hands-off reactivity used by Zone.js begins to show some weaknesses. Some of these challenges are:

  • Maintenance and performance safeguarding: Maintaining reactivity becomes more challenging as the application grows. This is because Zone.js relies on DOM events and async tasks to detect changes in the application state, but it doesn't know whether the state really changed. This causes it to trigger synchronization more often than it is supposed to.

  • Debugging reactivity: It is difficult to identify the root cause of reactivity problems. With Zone.js, it is challenging to know if the code breaks because it is outside the zone. This is a result of Zone.js’s challenging stack trace interpretation.

  • Cost of Zones with new Web APIs: Application loading cost and start-up time increase when new Web APIs are added. This will increase the application bundle size. Removing this dependency makes the application more efficient.

Introduction to Zoneless Applications

Zoneless applications do not use Zone.js for change detection. Rather, the component itself notifies Angular's change detection mechanism to update the UI after the completion of an async function.

It gives developers additional flexibility over the timing and mechanism of change detection, improving the application's performance and efficiency.

We will demonstrate with some examples without Zone.js that require change detection. To follow up on this tutorial, first install Angular 18 by running the command below.

npm install -g @angular/cli
Enter fullscreen mode Exit fullscreen mode

Run the command below in the command line to create a new Angular project.

ng new <filename>
Enter fullscreen mode Exit fullscreen mode

Open the file in your code editor. After that, disable Zone.js. We can do this by removing Zone.js from the polyfills in the angular.json file.

"polyfills": [
  "zone.js" // remove zone.js
]
Enter fullscreen mode Exit fullscreen mode

Then in the app.config.ts file, add provideExperimentalZonelessChangeDetection() in the providers array. After you've done it, you have successfully configured your Angular app to use the Zoneless feature for change detection instead of Zone.js.

import { ApplicationConfig } from '@angular/core';
import { provideExperimentalZonelessChangeDetection } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [
    provideExperimentalZonelessChangeDetection(),
    provideRouter(routes),
 ],
};
Enter fullscreen mode Exit fullscreen mode

Setting output in the component

After configuring our application to be Zoneless, we will test the change detection mechanism by defining a variable and updating its state.

In the code below, we update the PageNumber to 2 using the ngOnInit method. Angular's change detection mechanism detects this change and automatically updates the UI.

import { RouterOutlet } from '@angular/router';
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, CommonModule],
  template: `
    <div>Page Number: {{ PageNumber }}</div>
  `,
})
export class AppComponent implements OnInit {
  title = 'zoneless';
  PageNumber = 0;

  ngOnInit(): void {
    this.PageNumber += 2;
  }
}
Enter fullscreen mode Exit fullscreen mode

In the image below, the PageNumber variable is automatically updated from 0 to 2.

setting

Timer event change

Timer event change in Zoneless isn't automatically updated. It works like with OnPush as you'll need to manually trigger it because there's no automatic change detection as in Zone.js.

To test this, let's create a timer event using setTimeout() to update the PageNumber to 2 after 1 second.

@Component({
  selector: 'app-root',
  template: `
    <div>Page Number: {{ PageNumber }}</div>
  `,
})
export class AppComponent implements OnInit {
  title = 'zoneless';
  PageNumber = 0;

  ngOnInit(): void {
    setTimeout(() => {
      this.PageNumber += 2;
    }, 1000);
  }
}
Enter fullscreen mode Exit fullscreen mode

The image shows that the PageNumber variable is not set to 2 after 1 second.

image

This shows that Angular might not automatically detect asynchronous operations. To initialize it manually, ChangeDetectorRef can be applied to trigger the event handler.

To do this, inject ChangeDetectorRef into the AppComponent and use the changeDetectorRef.detectChanges() function to trigger the update.

import { ChangeDetectorRef, inject } from '@angular/core';

export class AppComponent implements OnInit {
  title = 'zoneless';
  changeDetectorRef = inject(ChangeDetectorRef);
  PageNumber = 0;

  ngOnInit(): void {
    setTimeout(() => {
      this.PageNumber += 2;
      this.changeDetectorRef.detectChanges();
    }, 1000);
  }
}
Enter fullscreen mode Exit fullscreen mode

The image shows that the PageNumber variable is updated to 2 after 1 second.

signal update

Signal Update

In Zoneless applications, the signal API automatically updates the reactive state to the UI, just as it does in Zone.js.

To test this, let's create a signal and initialize it with the value 5. Then update this value by calling the this.signal.set() after 1-second delay.

import { signal } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <div>Signal: {{ signal() }}</div>
  `,
})
export class AppComponent implements OnInit {
  title = 'zoneless';
  signal = signal(5);

  ngOnInit(): void {
    setTimeout(() => {
      this.signal.set(2);
    }, 1000);
  }
}
Enter fullscreen mode Exit fullscreen mode

The image shows that the signal is updated from 5 to 2 in the Zoneless application.

async pipe (1)

Event Change

The Angular detection mechanism detects when a user interacts with elements, in this case below a button, and triggers an update.

Let's create a property eventChange and set it to 0. In the template, we will define a button that, on clicking, will change the value of the event to 200.

@Component({
  selector: 'app-root',
  template: `
    <button (click)="eventChange = 200">Click here</button> 
    {{ eventChange }}
  `,
})
export class AppComponent {
  title = 'zoneless';
  eventChange = 0;
}
Enter fullscreen mode Exit fullscreen mode

In the image below, once the button is clicked the eventChange variable is automatically updated to 200.

event change

Async pipe

The async pipe subscribes to an Observable and returns the latest value emitted. When this new value is emitted, the async pipe automatically updates the UI after triggering the change detection.

In the code below, we create an Observable time$ that emits a value every second. The async pipe updates the displayed value, which subscribes to time$.

import { interval } from 'rxjs';

@Component({
  selector: 'app-root',
  template: `
    <div>Current Time: {{ time$ | async }}</div>
  `,
})
export class AppComponent {
  time$ = interval(1000); // Emits values every second
}
Enter fullscreen mode Exit fullscreen mode

The image below shows the value of time$, updated every second.

async pipe

It is vital to note that Zoneless change detection is still an experimental feature and major updates could be added to its API.

Conclusion

Angular moving towards Zoneless change detection will address some challenges with Zone.js such as maintenance, performance, and debugging, thereby making Angular applications perform more efficiently. We looked through examples where we needed to manually trigger updates using ChangeDetectorRef, and leverage async pipe for automatic updates. Zoneless change detection is still experimental but offers a promising alternative for building more scalable applications.

Additional Resources


Top comments (0)