How to fix change detection issues in Angular
Change detection issues in Angular cause errors, performance problems, and unexpected UI behavior requiring specific debugging techniques. With over 12 years of Angular development experience since 2014 and as the creator of CoreUI, I’ve fixed countless change detection bugs. Angular’s change detection system runs checks to update the view when data changes, but improper usage causes common issues. This approach identifies and resolves change detection errors and performance bottlenecks.
Fix Angular change detection issues by understanding the detection cycle, using proper lifecycle hooks, and optimizing detection strategy.
ExpressionChangedAfterItHasBeenCheckedError:
// child.component.ts - WRONG
import { Component, Input, OnInit } from '@angular/core'
@Component({
selector: 'app-child',
template: '<div>{{ value }}</div>'
})
export class ChildComponent implements OnInit {
@Input() value: string
ngOnInit() {
// This causes the error in parent
this.value = 'Changed value'
}
}
// parent.component.ts - WRONG
@Component({
selector: 'app-parent',
template: `
<div>
<p>Parent value: {{ childValue }}</p>
<app-child [value]="childValue"></app-child>
</div>
`
})
export class ParentComponent implements AfterViewInit {
childValue = 'Initial'
ngAfterViewInit() {
// Changing value after view is checked causes error
this.childValue = 'New value'
}
}
Fix with ChangeDetectorRef:
// parent.component.ts - FIXED
import { Component, AfterViewInit, ChangeDetectorRef } from '@angular/core'
@Component({
selector: 'app-parent',
template: `
<div>
<p>Parent value: {{ childValue }}</p>
<app-child [value]="childValue"></app-child>
</div>
`
})
export class ParentComponent implements AfterViewInit {
childValue = 'Initial'
constructor(private cdr: ChangeDetectorRef) {}
ngAfterViewInit() {
// Change value
this.childValue = 'New value'
// Manually trigger change detection
this.cdr.detectChanges()
}
}
Fix with setTimeout:
// Alternative fix using setTimeout
ngAfterViewInit() {
setTimeout(() => {
this.childValue = 'New value'
}, 0)
}
Change detection not triggered:
// service.ts
import { Injectable } from '@angular/core'
@Injectable({
providedIn: 'root'
})
export class DataService {
data: string[] = []
// WRONG: Modifications outside Angular zone not detected
updateDataWrong() {
setTimeout(() => {
this.data.push('New item')
// Angular doesn't know about this change
}, 1000)
}
// CORRECT: Use NgZone
updateDataCorrect(zone: NgZone) {
setTimeout(() => {
zone.run(() => {
this.data.push('New item')
})
}, 1000)
}
}
// component.ts - FIXED
import { Component, NgZone } from '@angular/core'
import { DataService } from './data.service'
@Component({
selector: 'app-data',
template: '<ul><li *ngFor="let item of service.data">{{ item }}</li></ul>'
})
export class DataComponent {
constructor(
public service: DataService,
private zone: NgZone
) {}
updateData() {
this.service.updateDataCorrect(this.zone)
}
}
Fix with ChangeDetectorRef.markForCheck:
// component.ts
import { Component, ChangeDetectorRef } from '@angular/core'
@Component({
selector: 'app-async-data',
template: '<div>{{ data }}</div>'
})
export class AsyncDataComponent {
data: string
constructor(private cdr: ChangeDetectorRef) {}
loadData() {
// External callback outside Angular zone
someExternalLibrary.fetchData((result) => {
this.data = result
// Manually mark for check
this.cdr.markForCheck()
})
}
}
OnPush strategy issues:
// child.component.ts - WRONG with OnPush
import { Component, Input, ChangeDetectionStrategy } from '@angular/core'
@Component({
selector: 'app-child',
template: '<div>{{ user.name }}</div>',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent {
@Input() user: any
}
// parent.component.ts - WRONG
@Component({
selector: 'app-parent',
template: '<app-child [user]="user"></app-child>'
})
export class ParentComponent {
user = { name: 'John' }
updateUser() {
// Mutation doesn't trigger OnPush detection
this.user.name = 'Jane'
}
}
Fix with immutable updates:
// parent.component.ts - FIXED
@Component({
selector: 'app-parent',
template: '<app-child [user]="user"></app-child>'
})
export class ParentComponent {
user = { name: 'John' }
updateUser() {
// Create new object reference
this.user = { ...this.user, name: 'Jane' }
}
}
Manual change detection:
import { Component, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core'
@Component({
selector: 'app-manual',
template: '<div>{{ count }}</div>',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ManualComponent {
count = 0
constructor(private cdr: ChangeDetectorRef) {}
increment() {
this.count++
// Manually trigger detection
this.cdr.markForCheck()
}
detach() {
// Completely detach from change detection
this.cdr.detach()
}
reattach() {
// Reattach to change detection
this.cdr.reattach()
}
detectChanges() {
// Run change detection once
this.cdr.detectChanges()
}
}
Debugging change detection:
import { ApplicationRef, NgZone } from '@angular/core'
import { Component } from '@angular/core'
@Component({
selector: 'app-debug',
template: '<div>Debug Component</div>'
})
export class DebugComponent {
constructor(
private appRef: ApplicationRef,
private zone: NgZone
) {
this.debugChangeDetection()
}
debugChangeDetection() {
// Log when zone becomes stable
this.zone.onStable.subscribe(() => {
console.log('Zone stable')
})
// Log when zone becomes unstable
this.zone.onUnstable.subscribe(() => {
console.log('Zone unstable')
})
// Check if app is stable
this.appRef.isStable.subscribe(stable => {
console.log('App stable:', stable)
})
}
}
Avoid change detection in loops:
// WRONG - getter called on every change detection
@Component({
selector: 'app-list',
template: '<div *ngFor="let item of getItems()">{{ item }}</div>'
})
export class ListComponent {
items = [1, 2, 3, 4, 5]
getItems() {
console.log('getItems called') // Called repeatedly
return this.items.filter(item => item > 2)
}
}
// FIXED - use property
@Component({
selector: 'app-list',
template: '<div *ngFor="let item of filteredItems">{{ item }}</div>'
})
export class ListComponent {
items = [1, 2, 3, 4, 5]
filteredItems = this.items.filter(item => item > 2)
updateItems() {
this.filteredItems = this.items.filter(item => item > 2)
}
}
// Or use pure pipe
@Pipe({
name: 'filter',
pure: true
})
export class FilterPipe implements PipeTransform {
transform(items: number[], threshold: number): number[] {
return items.filter(item => item > threshold)
}
}
// Usage
@Component({
template: '<div *ngFor="let item of items | filter:2">{{ item }}</div>'
})
Zone.js causing excessive checks:
// Run code outside Angular zone
import { Component, NgZone } from '@angular/core'
@Component({
selector: 'app-outside-zone',
template: '<canvas #canvas></canvas>'
})
export class OutsideZoneComponent {
constructor(private zone: NgZone) {}
startAnimation() {
// Run animation outside zone to avoid triggering change detection
this.zone.runOutsideAngular(() => {
const animate = () => {
// Animation logic
this.updateCanvas()
requestAnimationFrame(animate)
}
requestAnimationFrame(animate)
})
}
updateCanvas() {
// Update canvas without triggering change detection
}
}
Async pipe issues:
// WRONG - manual subscription
@Component({
selector: 'app-data',
template: '<div>{{ data }}</div>'
})
export class DataComponent implements OnInit, OnDestroy {
data: string
private subscription: Subscription
ngOnInit() {
this.subscription = this.dataService.getData().subscribe(data => {
this.data = data
// Need manual change detection if using OnPush
})
}
ngOnDestroy() {
this.subscription.unsubscribe()
}
}
// FIXED - use async pipe
@Component({
selector: 'app-data',
template: '<div>{{ data$ | async }}</div>',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DataComponent {
data$ = this.dataService.getData()
constructor(private dataService: DataService) {}
}
Best Practice Note
Use ChangeDetectorRef.detectChanges() to manually trigger detection after view initialization. Wrap external callbacks in NgZone.run() to ensure Angular detects changes. With OnPush strategy, use immutable data updates or markForCheck(). Avoid getters in templates—they’re called on every change detection cycle. Use async pipe for Observables—it handles subscription and change detection automatically. Run animations and frequent updates outside Angular zone with runOutsideAngular(). This is how we handle change detection in CoreUI Angular applications—understanding the detection cycle, using proper strategies, and optimizing for performance in production dashboards.



