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.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.

Answers by CoreUI Core Team