In Angular, the framework uses Zone.js to track and react to asynchronous operations. Skilled Angular developers know this enables Angular-based apps to detect changes based on updates the view gets on model changes.
However, most third-party libraries that are not optimized for Angular might manipulate the DOM directly, which results in Angular being unaware of any changes. This results in the view not getting updated properly.
Problem Overview
Angular runs change detection when:
- Events like clicks or input occur
- Timers (setTimeout, setInterval) fire
- HTTP calls complete
- Promises resolve
These are wrapped by Zone.js. But if a third-party library manipulates the DOM outside of Angular’s Zone, Angular won’t detect any need to refresh the UI.
Solution: NgZone or ChangeDetectorRef
Angular provides tools to manually trigger change detection:
- NgZone – Re-enters Angular’s zone after an external operation.
- ChangeDetectorRef.detectChanges() – Triggers change detection explicitly.
Example: Chart Library Updating DOM
Let’s say you’re using a third-party charting library (like Chart.js or ApexCharts), and after rendering a chart, you want to show a message or toggle a flag in the component.
Problem Code
@Component({ selector: 'app-chart', template: ` <div id="chart-container"></div> <p *ngIf="chartRendered">Chart rendered!</p> `
})
export class ChartComponent implements OnInit { chartRendered = false; ngOnInit() { thirdPartyRenderChart('#chart-container', () => { this.chartRendered = true; // DOM changes outside Angular // Angular won’t detect this change! }); }
}
Angular won’t detect the chartRendered = true change because the callback is executed outside its zone.
Fix 1: Using NgZone.run()
import { Component, NgZone, OnInit } from '@angular/core';
@Component({ selector: 'app-chart', template: ` <div id="chart-container"></div> <p *ngIf="chartRendered">Chart rendered!</p> `
})
export class ChartComponent implements OnInit { chartRendered = false; constructor(private ngZone: NgZone) {} ngOnInit() { thirdPartyRenderChart('#chart-container', () => { this.ngZone.run(() => { this.chartRendered = true; // Now Angular will detect it! }); }); }
}
NgZone.run() ensures that the code runs inside Angular’s zone, so change detection is triggered.
Fix 2: Using ChangeDetectorRef.detectChanges()
import { Component, ChangeDetectorRef, OnInit } from '@angular/core';
@Component({ selector: 'app-chart', template: ` <div id="chart-container"></div> <p *ngIf="chartRendered">Chart rendered!</p> `
})
export class ChartComponent implements OnInit { chartRendered = false; constructor(private cdr: ChangeDetectorRef) {} ngOnInit() { thirdPartyRenderChart('#chart-container', () => { this.chartRendered = true; this.cdr.detectChanges(); // Manually trigger change detection }); }
}
Use this when you know Angular won’t auto-detect changes and you want fine-grained control.
When to Use Which?
Situation | Use |
DOM changes in callback from third-party lib | NgZone.run() |
For delaying or controlling change detection manually | ChangeDetectorRef.detectChanges() |
You’re doing batch DOM updates outside Angular | NgZone.runOutsideAngular() + NgZone.run() |
Bonus: NgZone.runOutsideAngular()
Sometimes you intentionally avoid Angular change detection (e.g., rendering a large chart to improve performance), then re-enter Angular once done:
this.ngZone.runOutsideAngular(() => { thirdPartyRenderChart('#chart', () => { this.ngZone.run(() => { this.chartRendered = true; }); });
});
To sum it all up
- Third-party libraries often modify the DOM outside Angular’s zone.
- Angular can’t identify or detect changes automatically in such cases.
- Use NgZone.run() to re-enter Angular’s zone and ensure change detection.
- Or use ChangeDetectorRef.detectChanges() for explicit detection.
- For performance, combine with runOutsideAngular().