Introduction

Angular’s default change detection strategy (`Default`) checks for changes in all components during each change detection cycle. This can be inefficient for large applications. To optimize performance, developers use `ChangeDetectionStrategy.OnPush`, which only triggers when component inputs change or an event handler modifies state. However, OnPush can sometimes fail to detect changes, causing UI updates to be skipped. This article explores the root causes, debugging techniques, and solutions to resolve this issue effectively.

Understanding Angular’s OnPush Change Detection

When a component is set to `ChangeDetectionStrategy.OnPush`, Angular updates the view only if:

  • The component receives new input references.
  • An event handler modifies state.
  • Change detection is manually triggered via `markForCheck()` or `detectChanges()`.

However, when updates are missed, UI inconsistencies arise. Let’s examine why this happens.

Common Causes of OnPush Change Detection Issues

1. Mutating Input Objects Instead of Replacing Them

OnPush relies on reference checks for detecting changes. If an object’s properties are updated without changing its reference, Angular will not detect the modification.

Problematic Code

@Component({
  selector: 'app-child',
  template: '{{ data.value }}',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent {
  @Input() data: { value: string };
}

@Component({ selector: 'app-parent', template: '' })
export class ParentComponent {
  obj = { value: 'Hello' };

  updateValue() {
    this.obj.value = 'Updated'; // No UI update!
  }
}

Solution: Replace the Object Instead of Mutating

updateValue() {
  this.obj = { ...this.obj, value: 'Updated' }; // Triggers change detection
}

2. Change Detection Fails in Observables

If an observable emits new values asynchronously, Angular may not detect changes unless the component subscribes correctly.

Problematic Code

@Component({ selector: 'app-child', template: '{{ data }}', changeDetection: ChangeDetectionStrategy.OnPush })
export class ChildComponent {
  @Input() data: string;
}

@Component({ selector: 'app-parent', template: '' })
export class ParentComponent {
  data$ = new BehaviorSubject('Initial');
}

Solution: Ensure Change Detection Runs

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

constructor(private cdr: ChangeDetectorRef) {}

ngOnInit() {
  this.data$.subscribe(() => {
    this.cdr.markForCheck(); // Ensures UI updates
  });
}

3. DOM Events Inside Child Components Do Not Trigger Change Detection

With OnPush, UI updates may not occur if changes happen outside Angular’s zone.

Solution: Use `NgZone` to Run Change Detection

import { NgZone } from '@angular/core';
constructor(private zone: NgZone) {}

handleEvent() {
  this.zone.run(() => {
    this.data = 'Updated';
  });
}

Advanced Debugging Techniques

1. Use Angular’s Debugging Tools

Run `ng.profiler.timeChangeDetection()` in the browser console to measure change detection execution.

2. Log Change Detection Execution

ngDoCheck() {
  console.log('Change detection running in component');
}

3. Use `detach()` and `reattach()` for Performance Optimization

import { ChangeDetectorRef } from '@angular/core';
constructor(private cdr: ChangeDetectorRef) {}

optimizeComponent() {
  this.cdr.detach(); // Stop change detection
  setTimeout(() => {
    this.cdr.reattach(); // Resume change detection
  }, 1000);
}

Conclusion

`ChangeDetectionStrategy.OnPush` improves Angular performance but requires careful handling to prevent UI inconsistencies. By avoiding direct object mutations, handling observables correctly, and using `markForCheck()`, developers can ensure reliable UI updates. Debugging tools like `ng.profiler.timeChangeDetection()` help analyze change detection behavior.

Frequently Asked Questions

1. Why is my Angular OnPush component not updating?

Ensure you are replacing input objects instead of mutating them. Also, check if `markForCheck()` is needed for async updates.

2. Does OnPush improve Angular performance?

Yes, it minimizes unnecessary checks by only running when input references change, reducing rendering overhead.

3. How can I trigger change detection manually?

Use `markForCheck()` to schedule a check or `detectChanges()` for immediate execution.

4. Why does an async pipe not trigger OnPush?

Async pipes run outside change detection if the observable does not emit new object references.

5. When should I use `ChangeDetectorRef.detach()`?

Use it to pause change detection for performance-sensitive components and manually trigger updates when needed.