Change Detection
Change detection is the mechanism Angular uses to determine when to update the DOM based on changes in your application state. Understanding and optimizing change detection is crucial for building performant Angular applications.
How Change Detection Works
By default, Angular checks every component in the component tree whenever any event occurs (clicks, HTTP requests, timers, etc.). This ensures the UI stays in sync with application state but can impact performance in large applications.
Change Detection Strategies
Angular provides two change detection strategies:
Default Strategy
Checks the component and all its children on every change detection cycle:
import { Component } from '@angular/core';
@Component({
templateUrl: './my-component.html',
// Default strategy (implicit)
})
export class MyComponent {}
OnPush Strategy
Only checks the component when:
- An
@Input() reference changes
- An event originates from the component or its children
- Manual change detection is triggered
- An async pipe receives a new value
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
@Component({
templateUrl: './counter-page.html',
styles: `
button {
padding: 5px;
margin: 5px 10px;
width: 75px;
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CounterPage {
counter = 10;
counterSignal = signal(10);
incremento(value: number) {
this.counter += value;
this.counterSignal.update((current) => current + value);
}
resetContador() {
this.counter = 0;
this.counterSignal.set(0);
}
}
Source: src/app/pages/counter/counter-page.ts:1-30
OnPush strategy significantly improves performance by reducing unnecessary checks. It’s especially effective when used with signals.
Signals and Change Detection
Signals work seamlessly with OnPush change detection. When a signal value changes, Angular automatically marks the component for checking:
export class CounterPage {
counterSignal = signal(10);
incremento(value: number) {
// Signal update automatically triggers change detection
this.counterSignal.update((current) => current + value);
}
}
In Templates
When you use signals in templates, Angular tracks the dependency:
<h1>Counter: {{ counter }}</h1>
<h1>Counter Signal: {{ counterSignal() }}</h1>
<hr/>
<button (click)="incremento(1)">+1</button>
<button (click)="incremento(-1)">-1</button>
<button (click)="resetContador()">Reset</button>
Source: src/app/pages/counter/counter-page.html:1-6
Notice how counter and counterSignal() are both displayed. The signal (with parentheses) automatically notifies Angular when it changes, making it perfect for OnPush components.
OnPush with Traditional Properties
With OnPush strategy, traditional property updates might not trigger UI updates:
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CounterPage {
// Traditional property
counter = 10;
incremento(value: number) {
// This updates the value but might not update the UI
this.counter += value;
}
}
The template will update because the event (click) originates from the component, but external changes won’t be detected.
OnPush with Complex Objects
When working with objects or arrays, create new references to trigger change detection:
import { Component, signal } from '@angular/core';
interface Character {
id: number;
name: string;
power: number;
}
@Component({
templateUrl: './dragonball-super-page.html',
})
export class DragonballSuperPage {
characters = signal<Character[]>([
{ id: 1, name: 'Goku', power: 9001 },
{ id: 2, name: 'Vegeta', power: 8000 },
]);
addCharacter() {
const newCharacter: Character = {
id: this.characters().length + 1,
name: this.name(),
power: this.power(),
};
// Create new array reference - essential for change detection
this.characters.update(chars => [...chars, newCharacter]);
}
}
Source: src/app/pages/dragonball-super/dragonball-super-page.ts:1-37
Never mutate objects or arrays directly inside signals:// Wrong - mutation doesn't trigger updates
this.characters().push(newCharacter);
// Correct - new reference triggers updates
this.characters.update(chars => [...chars, newCharacter]);
When to Use OnPush
OnPush strategy is ideal when:
Use OnPush when:
- Working with signals for state management
- Component relies on immutable data patterns
- Using async pipe with observables
- Building performance-critical features
- Component has many children that don’t need frequent updates
- Input data changes infrequently
OnPush strategy provides significant performance improvements:
Before OnPush
// Component checked on every change detection cycle
// Even if nothing changed in this component
@Component({
templateUrl: './my-component.html',
})
export class MyComponent {
data = signal([]);
}
After OnPush
// Component only checked when necessary
@Component({
templateUrl: './my-component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MyComponent {
data = signal([]);
}
Manual Change Detection
Sometimes you need to manually trigger change detection:
import { ChangeDetectorRef, Component } from '@angular/core';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './my-component.html',
})
export class MyComponent {
constructor(private cdr: ChangeDetectorRef) {}
updateFromExternalSource(data: any) {
this.processData(data);
// Manually mark for check
this.cdr.markForCheck();
}
}
With signals, manual change detection is rarely needed. Signals automatically handle change detection for you.
Async Pipe with OnPush
The async pipe works perfectly with OnPush strategy:
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { AsyncPipe } from '@angular/common';
@Component({
imports: [AsyncPipe],
template: '<div>{{ data$ | async }}</div>',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MyComponent {
data$: Observable<any>;
}
The async pipe automatically marks the component for check when the observable emits.
Common Pitfalls
Mutating Objects
// Wrong - doesn't trigger change detection
this.characters().push(newItem);
// Correct - creates new reference
this.characters.update(chars => [...chars, newItem]);
Forgetting to Call Signals
<!-- Wrong - doesn't call the signal -->
<h1>{{ counterSignal }}</h1>
<!-- Correct - calls signal as function -->
<h1>{{ counterSignal() }}</h1>
Mixing Strategies
// Avoid mixing signal and non-signal approaches
export class CounterPage {
counter = 10; // Traditional property
counterSignal = signal(10); // Signal property
// Inconsistent updates
incremento(value: number) {
this.counter += value; // No automatic detection
this.counterSignal.update((current) => current + value); // Automatic detection
}
}
Source: src/app/pages/counter/counter-page.ts:18-23
Choose one approach: either use signals everywhere or stick with traditional change detection patterns. Mixing them can lead to confusion and bugs.
Best Practices
Change Detection Best Practices:
- Use OnPush strategy with signals for optimal performance
- Always create new references for objects and arrays in signals
- Let signals handle change detection automatically
- Use async pipe for observables in OnPush components
- Avoid manual change detection when using signals
- Call signals as functions in templates:
{{ signal() }}
- Keep components pure and predictable
Next Steps
- Learn about Signals for reactive state management
- Explore Components architecture
- Understand Routing for navigation between views