Boosting Angular efficiency : Stop using manual change detection

A Practical Guide to Refining Change Detection in Angular

Ahmed Bouhlel
Dev Genius

--

In previous discussions on Angular’s change detection, we’ve delved into what manual change detection calls entail. In this article, I’ll demonstrate their mechanics and share how I’ve successfully eliminated them from my Angular applications.

We’ll begin with a straightforward example: a service designed to fetch a user’s name, which I then aim to display within a component.


@Injectable({ providedIn: 'root' })
export class ProfileService {
httpClient = inject(HttpClient);
getProfile() {
return this.httpClient.get<string>('/api/profile');
}
}

@Component({
template: `<div>{{ displayName }}</div>`,
})
export class ProfileComponent {
#profileService = inject(ProfileService);
displayName: string = '';

ngOnInit() {
this.profileService.getProfile()
.pipe(take(1))
.subscribe(profile => {
this.displayName = profile.name;
});
}
}

When you run this code in an Angular application, it functions seamlessly, courtesy of zone.js. This library plays a crucial role by intercepting XHR requests like the ones made in our ProfileService. Whenever data changes as a result of these requests, zone.js informs Angular to initiate change detection. This mechanism is what allows the updated displayName to be displayed in the view

Now suppose you want to enhance your component’s performance by using ChangeDetectionStrategy.OnPush. Here’s how you might set it up:

@Component({
template: `<div>{{ displayName}}</div>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProfileComponent { /*...*/ }

With OnPush enabled, the component's view won't update automatically when the displayName changes because Angular isn't being notified about the data change. This is where you'd typically see manual change detection calls come into play.

To address this, you can use the ChangeDetectorRef's markForCheck method, which tells Angular to mark this component as 'dirty' and refresh the bindings and interpolations during the next change detection cycle:

export class ProfileComponent{
cdr = inject(ChangeDetectorRef);

ngOnInit() {
this.profileService.getProfile()
.pipe(take(1))
.subscribe(profile=> {
this.displayName = profile.name;
this.cdr.markForCheck();
});
}
}

This setup works well until new requirements arise. For instance, if you need to update the displayName and introduce a button that triggers this update, you would need to call markForCheck again to ensure the view updates:

export class ProfileComponent{
displayName?: string;

handleNameChange(newDisplayName: string) {
this.profileService.changeName(newDisplayName)
.pipe(take(1))
.subscribe(name => {
this.displayName= name;
this.cdr.markForCheck();
});
}
}

This process can quickly become cumbersome and error-prone, potentially leading to missed updates and bugs.

To streamline this, integrating RxJS and observables, along with the async pipe in your template, proves beneficial.

The async pipe manages the subscription and automatically marks your component to be checked whenever the data changes, alleviating the need for manual markForCheck calls.

Our component setup using AsyncPipe and BehaviorSubject would look like this:

@Component({
template: `<div>{{ displayName$ | async }}</div>`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [AsyncPipe]
})
export class ProfileComponent{
profileService= inject(ProfileService);
cdr = inject(ChangeDetectorRef);

displayName$ = new BehaviorSubject<string>('');

ngOnInit() {
this.profileService.getProfile()
.pipe(take(1))
.subscribe(profile=> {
this.displayName$.next(profile.displayName);
});
}

handleNameChange(newName: string) {
this.profileService.changeName(newName)
.pipe(take(1))
.subscribe(name => {
this.displayName$.next(name);
});
}
}

With this structure, there’s no longer a need to manually invoke markForCheck. The use of the async pipe ensures that the view will automatically update when the data changes. However, managing BehaviorSubjects and using the async pipe might seem a bit complex. This is precisely where Angular signals can offer a more streamlined approach.

Angular signals introduce a revolutionary method to manage state within applications. They are designed to simplify the coding experience while simultaneously enhancing performance.

By adopting a signal-based approach, Angular can optimize change detection and state management, making the framework more intuitive and performant. Signals allow developers to write cleaner, more efficient code by reducing boilerplate and focusing on reactivity.

Step-by-step guide to refining change detection in Angular

  1. Install and configure ESLint for Angular
  • To start, if you’re not already using ESLint in your project, consider installing the RxAngular ESLint plugin. This tool helps identify manual change detection calls and suggests improvements for building reactive, performant, and Zone-less Angular applications.
  • Installation
npm install --save-dev @rx-angular/eslint-plugin
  • Enable the ESLint Rule: Add the following configuration to your ESLint settings.
{
"parser": "@typescript-eslint/parser",
"plugins": ["@rx-angular"],
"rules": {
"@rx-angular/no-explicit-change-detection-apis": "error",
}
}

2. Identify why manual change detection was needed

In scenarios where ChangeDetectionStrategy.OnPush is used, Angular needs explicit notifications when data changes. For example:

displayName: string = '';

ngOnInit() {
this.profileService.getProfile()
.pipe(take(1))
.subscribe(profile=> {
this.displayName= profile.displayName;
this.cdr.markForCheck();
});
}

3. Replace Traditional State Management with Signals

Transition from using mutable state variables to using signals. This change helps enforce immutability and reactivity:

  • Before :
displayName: string = '';
  • After :
readonly displayName= signal('');

4. Update component logic to profile signals correctly

Adjust your component’s logic to interact with signals properly. This ensures type safety and reactivity:

ngOnInit() {
this.ProfileService.getProfile()
.pipe(take(1))
.subscribe(profile=> {
// ✅ Correctly setting the value using the set method
this.displayName.set(profile.name);
});
}

5. Update the template to reflect signal values

Finally, modify your template to display the signal values correctly:

  • Before :
<div>{{ displayName}}</div>
  • After :
<div>{{ displayName() }}</div>

Why This Works:

  • Utilizing signals in Angular not only facilitates the automatic triggering of change detection when the state changes but also integrates smoothly in a zoneless environment, initiating the necessary change detection cycles. This means that whenever a signal’s state is updated, Angular knows to update the view accordingly without manual intervention.

By following these steps, you effectively remove manual change detection calls from your Angular application, leading to cleaner, more maintainable, and performant code.

Thank you for reading !

If you found this article interesting and helpful, and you’re keen to dive deeper into Angular, consider supporting my work. You can buy me a coffee ☕️ to keep the content coming.

For regular updates on the latest in Angular — including news, signals, docker, spring, updates, and much more — follow me on X (formerly Twitter) @Ahmed_bouhlel . Your support and engagement are greatly appreciated ! 💎

--

--

Senior full-stack software enginering with extensive experience in multi programming languages, including Java, Angular& various modules of the Spring Framework