Skip to main content

Angular Change Detection

Introduction

Change Detection is one of Angular's core features that determines when and how the user interface should be updated to reflect changes in your application's data. It's the mechanism responsible for keeping your views in sync with your component's state. While Angular handles most change detection automatically, understanding how it works is crucial for building performant applications, especially as they grow in complexity.

In this guide, we'll explore:

  • How Angular's change detection works
  • The default change detection strategy
  • OnPush change detection strategy
  • Manual change detection control
  • Performance optimization techniques

How Angular Change Detection Works

The Basics

At its core, Angular's change detection is a process that checks if the data bound to your templates has changed and updates the DOM accordingly. This process is triggered by various events such as:

  • User events (clicks, input, etc.)
  • HTTP requests
  • Timers (setTimeout, setInterval)
  • Promises and Observables

When any of these events occur, Angular runs a change detection cycle, checking all components from top to bottom in the component tree.

Zone.js: Angular's Secret Weapon

Angular uses a library called Zone.js to detect when asynchronous operations complete. Zone.js patches all common asynchronous operations in the browser (like setTimeout, DOM events, and XMLHttpRequest) to notify Angular when something happens that might require UI updates.

typescript
// Behind the scenes, Zone.js wraps operations like this:
setTimeout(() => {
// Angular knows when this executes and triggers change detection
this.data = newValue;
}, 1000);

Default Change Detection Strategy

By default, Angular uses a strategy called Default Change Detection. In this strategy, whenever a change detection cycle runs, Angular checks every component in the application, starting from the root component and traversing down the component tree.

Let's see how this works in a simple example:

typescript
import { Component } from '@angular/core';

@Component({
selector: 'app-counter',
template: `
<p>Count: {{ count }}</p>
<button (click)="increment()">Increment</button>
`
})
export class CounterComponent {
count = 0;

increment() {
this.count++;
// When this method completes, Angular detects the change
// and updates the view automatically
}
}

In this example, when the user clicks the button, the increment() method runs, the count property changes, and Angular automatically updates the DOM to display the new value.

OnPush Change Detection Strategy

As applications grow, checking every component on every change detection cycle becomes inefficient. This is where the OnPush strategy comes in.

The OnPush strategy tells Angular to check a component and its children only if:

  1. An input property reference changes (not just its internal properties)
  2. An event originates from the component or its children
  3. You manually trigger change detection

Let's modify our previous example to use OnPush:

typescript
import { Component, ChangeDetectionStrategy } from '@angular/core';

@Component({
selector: 'app-counter',
template: `
<p>Count: {{ count }}</p>
<button (click)="increment()">Increment</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
count = 0;

increment() {
this.count++;
// With OnPush, the view will still update because the event originated
// from within this component (the button click)
}
}

Using OnPush with @Input Properties

When using OnPush with components that receive data via @Input properties, you need to be careful about mutation vs. replacement:

typescript
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';

@Component({
selector: 'app-user-profile',
template: `
<div>
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserProfileComponent {
@Input() user: { name: string, email: string };
}

In the parent component:

typescript
@Component({
selector: 'app-parent',
template: `
<app-user-profile [user]="currentUser"></app-user-profile>
<button (click)="updateUserCorrectly()">Update Correctly</button>
<button (click)="updateUserIncorrectly()">Update Incorrectly</button>
`
})
export class ParentComponent {
currentUser = { name: 'John', email: '[email protected]' };

// This will NOT trigger change detection in the child component
updateUserIncorrectly() {
this.currentUser.name = 'Jane'; // Mutating the existing object
}

// This WILL trigger change detection in the child component
updateUserCorrectly() {
this.currentUser = {
...this.currentUser,
name: 'Jane'
}; // Creating a new object reference
}
}

Manual Change Detection Control

Sometimes you need more control over when change detection runs. Angular provides tools for this:

ChangeDetectorRef

The ChangeDetectorRef service allows you to manually control the change detection in your components.

typescript
import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';

@Component({
selector: 'app-manual-counter',
template: `
<p>Count: {{ count }}</p>
<button (click)="increment()">Increment</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ManualCounterComponent {
count = 0;

constructor(private cd: ChangeDetectorRef) {}

increment() {
this.count++;
// Manually trigger change detection for this component
this.cd.markForCheck();
}

detach() {
// Detach this component from change detection
this.cd.detach();
}

reattach() {
// Reattach and check this component
this.cd.reattach();
}

detectChanges() {
// Run change detection for this component only
this.cd.detectChanges();
}
}

The main methods provided by ChangeDetectorRef are:

  • markForCheck(): Marks the component and all its ancestors for checking in the next change detection cycle
  • detach(): Detaches the component from the change detection tree
  • reattach(): Reattaches a previously detached component
  • detectChanges(): Checks this component and its children immediately

Practical Application: Data Dashboard

Let's see a real-world example of optimizing change detection in a dashboard application that displays multiple widgets:

typescript
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';

@Component({
selector: 'app-data-widget',
template: `
<div class="widget">
<h3>{{ title }}</h3>
<div class="content">
<ng-content></ng-content>
</div>
<div class="footer">
Last updated: {{ lastUpdated | date:'medium' }}
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DataWidgetComponent {
@Input() title: string;
@Input() lastUpdated: Date;
}

@Component({
selector: 'app-dashboard',
template: `
<div class="dashboard">
<app-data-widget
[title]="'User Statistics'"
[lastUpdated]="userStats.lastUpdated">
<app-user-chart [data]="userStats.data"></app-user-chart>
</app-data-widget>

<app-data-widget
[title]="'Traffic Overview'"
[lastUpdated]="trafficData.lastUpdated">
<app-traffic-chart [data]="trafficData.data"></app-traffic-chart>
</app-data-widget>

<app-data-widget
[title]="'Recent Activities'"
[lastUpdated]="activities.lastUpdated">
<app-activity-list [activities]="activities.data"></app-activity-list>
</app-data-widget>
</div>
`
})
export class DashboardComponent {
userStats = {
data: [],
lastUpdated: new Date()
};

trafficData = {
data: [],
lastUpdated: new Date()
};

activities = {
data: [],
lastUpdated: new Date()
};

updateUserStats(newData) {
// Create new reference to trigger OnPush change detection
this.userStats = {
data: newData,
lastUpdated: new Date()
};
}

// Similar methods for other widgets...
}

In this dashboard, each widget uses OnPush change detection, so it only updates when its input properties change. This is much more efficient than having every widget update every time any data changes.

Performance Optimization Tips

  1. Use OnPush where appropriate: Apply it to components that don't need to update frequently or only depend on input changes.

  2. Avoid complex expressions in templates: Move complex calculations to component methods or use pure pipes.

    typescript
    // Avoid this in templates
    {{ getComplexCalculation(item) }}

    // Better: Use a pure pipe
    {{ item | complexCalculationPipe }}
  3. Use trackBy with ngFor: This helps Angular identify which items have changed in a list.

    html
    <div *ngFor="let item of items; trackBy: trackByFn">{{ item.name }}</div>
    typescript
    trackByFn(index, item) {
    return item.id; // unique identifier
    }
  4. Run outside NgZone for non-UI updates: Some operations don't need to trigger change detection.

    typescript
    constructor(private ngZone: NgZone) {}

    runExpensiveProcess() {
    this.ngZone.runOutsideAngular(() => {
    // This code won't trigger change detection
    setInterval(() => {
    this.calculateSomething();
    }, 500);
    });
    }
  5. Use Async Pipe: It automatically subscribes and unsubscribes from observables and triggers change detection when new values arrive.

    html
    <div *ngFor="let item of items$ | async">{{ item.name }}</div>

Summary

Angular's change detection is a powerful system that efficiently updates the DOM when your data changes. Understanding how it works helps you build more performant applications:

  • Default change detection checks all components from top to bottom on any event
  • OnPush change detection only checks components when input references change or events happen in the component
  • ChangeDetectorRef provides manual control over change detection when needed
  • Use immutable data patterns with OnPush for best performance
  • Apply optimization techniques like trackBy and async pipe for better performance

By leveraging these patterns and strategies, you can make your Angular applications faster and more responsive, especially as they grow in size and complexity.

Additional Resources

Exercises

  1. Create a component with OnPush change detection and experiment with different ways of updating its data to see when the view updates.
  2. Implement a dashboard with multiple widgets that only update when their specific data changes.
  3. Use Chrome DevTools performance tab to compare the performance of an application with and without OnPush change detection.
  4. Create a component that deliberately detaches itself from change detection and only updates on specific user actions.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)