Angular

Change Detection

15 Questions

Change detection is the mechanism Angular uses to keep the DOM in sync with component data. When data changes, Angular checks the component tree and updates the DOM accordingly.

Why it matters: Tests understanding of Angular's core architecture. Shows you know how Angular keeps UI synchronized with data.

Real applications: Every user interaction, HTTP response, or timer triggers change detection. Understanding this is crucial for optimizing performance in large applications with many components.

Common mistakes: Developers don't realize change detection runs constantly. They mutate data without creating new references in OnPush components. They over-check performance without understanding what triggers detection.

Default: Angular checks the entire component tree on every change detection cycle.

OnPush: Angular only checks the component when its @Input references change, an event fires within the component, or change detection is manually triggered.

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush
})

Why it matters: Tests knowledge of performance optimization strategies. Shows you understand how to reduce unnecessary checks.

Real applications: Large dashboards use OnPush on most components to skip unnecessary checks. Default strategy used only for simple components where performance doesn't matter.

Common mistakes: Developers don't know OnPush exists. They default to checking everything. They use OnPush but don't create new references for data changes.

Zone.js is a library that patches async APIs (setTimeout, Promise, addEventListener, XHR) to notify Angular when async operations complete, triggering change detection automatically.

Why it matters: Tests understanding of how Angular automatically detects changes. Shows you know the machinery behind automatic change detection.

Real applications: Zone.js patches all browser APIs so Angular knows when to run change detection. Without it, you'd manually call markForCheck() for every async operation.

Common mistakes: Developers don't realize Zone.js patches browsers APIs. They think Angular magically knows when to re-render. They don't know they can use runOutsideAngular() to bypass Zone.js.

markForCheck() marks a component and its ancestors for checking in the next change detection cycle. Used with OnPush when data changes without @Input reference change.

constructor(private cdr: ChangeDetectorRef) {}

updateData() {
  this.data.push(newItem); // Mutation - OnPush won't detect
  this.cdr.markForCheck(); // Tell Angular to check
}

Why it matters: Tests understanding of manual change detection with OnPush. Shows you know how to handle mutations in OnPush components.

Real applications: Real-time data updates (WebSocket messages), array mutations that need UI reflection, triggering parent component checks after internal state changes.

Common mistakes: Developers forget to call markForCheck() after mutations. They use detectChanges() instead when markForCheck() is appropriate. They don't understand the difference between marking for next cycle vs immediate check.

detectChanges() immediately triggers change detection for the component and its children without waiting for the next cycle. Unlike markForCheck(), it runs the check right now. This is useful after programmatic data changes that need to be reflected in the DOM instantly. It only checks the current component subtree, not the entire application.
this.value = 'updated';
this.cdr.detectChanges(); // Immediately update DOM
Use detectChanges() sparingly and only when immediate DOM updates are required. Prefer markForCheck() in most cases to let Angular batch checks efficiently.

OnPush skips change detection for a component subtree unless: an @Input reference changes, an event originates from the component, an Observable linked to the async pipe emits, or markForCheck/detectChanges is called. This dramatically reduces checks in large apps.

Why it matters: Tests understanding of optimization through OnPush strategy. Shows you know which triggers cause re-checks in OnPush mode.

Real applications: List items use OnPush to skip checking 1000 items when only 5 changed. Nested components skip checks when parent data hasn't changed. Dashboard widgets skip checks independently.

Common mistakes: Developers use OnPush with mutable data, wondering why changes don't show up. They don't realize @Input reference change is required. They don't use immutable patterns with OnPush.

The async pipe automatically subscribes, gets values, and calls markForCheck() when new values arrive, making it perfect for OnPush components.

<div *ngIf="user$ | async as user">
  {{ user.name }}
</div>

Why it matters: Tests understanding of reactive patterns with OnPush. Shows you know the best way to use Observables with OnPush efficiently.

Real applications: User profiles load data via Observable, async pipe triggers detection when data arrives. Real-time dashboards receive WebSocket data through async pipes in OnPush components.

Common mistakes: Developers manually subscribe to Observables instead of using async pipe. They create memory leaks by forgetting to unsubscribe. They don't realize async pipe auto-triggers markForCheck().

detach() removes a component from change detection entirely. reattach() adds it back. Useful for components that rarely change.

this.cdr.detach(); // Stop checking
// ... later when update needed
this.cdr.reattach();
this.cdr.detectChanges();

Why it matters: Tests knowledge of extreme performance optimization. Shows you understand full control over change detection.

Real applications: Charts that update infrequently, static content that rarely changes, performance-critical components in data grids.

Common mistakes: Developers forget to reattach() and component gets stuck unchanged. They use detach() too liberally without understanding implications. They don't remember to call detectChanges() after reattach().

NgZone.runOutsideAngular() runs code outside Angular's zone so it doesn't trigger change detection. Useful for performance-heavy animations or frequent events.

constructor(private ngZone: NgZone) {}

ngOnInit() {
  this.ngZone.runOutsideAngular(() => {
    window.addEventListener('mousemove', this.onMouseMove);
  });
}

Why it matters: Tests knowledge of NgZone for performance optimization. Shows you can prevent unnecessary change detection for frequent events.

Real applications: Mouse move tracking that fires 60+ times per second, scroll event handlers, throttled animations, tracking without UI updates.

Common mistakes: Developers don't use runOutsideAngular() and 1000 change detection cycles trigger per second. They attach handlers inside Angular's zone causing performance degradation. They forget they need ngZone.run() if they later want updates to show.

Change detection is triggered by DOM events (click, input, submit), HTTP responses, timers (setTimeout, setInterval), Promises resolving, and any async operation patched by zone.js. Essentially, any asynchronous browser operation triggers Angular's change detection. This ensures the DOM stays in sync with component data automatically. With OnPush, only specific triggers cause checks.
// All of these trigger change detection:
button.click();                    // DOM event
this.http.get('/api').subscribe(); // HTTP response
setTimeout(() => {}, 1000);        // Timer
Promise.resolve().then(() => {});  // Promise
Zone.js monkey-patches these browser APIs to intercept async operations. Each intercepted operation triggers Angular's ApplicationRef.tick() which starts the change detection cycle.

Why it matters: Tests understanding of what triggers change detection. Shows you know async operations are the primary trigger.

Real applications: Button clicks trigger detection, HTTP responses trigger detection, timers trigger detection - essentially all async operations.

Common mistakes: Developers think change detection is triggered by other events. They don't realize Zone.js patches all browser APIs. They try to manually trigger detection when event-based is automatic.

markForCheck() marks the component and all its ancestors as needing to be checked in the next change detection cycle. It does not run detection immediately. detectChanges() immediately runs change detection for the component and its children right now. Use markForCheck with OnPush strategy when data changes without a new @Input reference.
// markForCheck: schedule check for next cycle
this.cdr.markForCheck();

// detectChanges: run check immediately right now
this.cdr.detectChanges();
Prefer markForCheck() in most cases because it lets Angular batch checks efficiently. Use detectChanges() only when you need the DOM updated immediately.

Why it matters: Tests understanding of efficient change detection patterns. Shows you know when to batch vs immediate checks.

Real applications: Component state changes use markForCheck for next cycle, form updates with async pipe use markForCheck, real-time data uses detectChanges for immediate display.

Common mistakes: Developers use detectChanges everywhere harming performance. They call markForCheck() without understanding it's not immediate. They don't understand batching benefits.

With the default strategy, Angular checks every component in the entire tree on every change detection cycle, even if nothing changed. With OnPush, Angular skips checking a component and its entire subtree unless specific triggers occur. These triggers are: @Input reference change, event from within the component, async pipe emission, or manual markForCheck/detectChanges. In large applications, this dramatically reduces the number of checks per cycle.
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class PerformantComponent {
  @Input() data!: ReadonlyArray<Item>; // immutable reference
}
OnPush works best with immutable data patterns where you replace objects/arrays instead of mutating them. Mutations are not detected because the reference stays the same.

Why it matters: Tests understanding of OnPush immutability requirement. Shows you know why immutable patterns matter.

Real applications: NgRx stores use immutable patterns with OnPush, list components use new array references when adding items, form updates create new entity objects.

Common mistakes: Developers use OnPush with mutable data and wonder why changes don't show. They mutate arrays with push() instead of spreading. They don't create new object references.

The async pipe is the best companion for OnPush components. When a new value arrives from the Observable, the async pipe automatically calls markForCheck() on the component. This triggers change detection for that component and its ancestors without any manual intervention. It also handles subscribing and unsubscribing automatically, preventing memory leaks.
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: '<div *ngFor="let user of users$ | async">{{ user.name }}</div>'
})
export class UserListComponent {
  users$ = this.http.get<User[]>('/api/users');
}
The async pipe makes OnPush components work reactively without manual ChangeDetectorRef calls. It is the cleanest pattern for consuming Observables in templates.

Why it matters: Tests understanding of reactive patterns with OnPush. Shows you know the best practice for Observables.

Real applications: User profiles load from API with async pipe triggering detection, dashboards receive WebSocket data through async pipes, lists update with async pagination.

Common mistakes: Developers manually subscribe to Observables causing memory leaks. They don't use async pipe and manually call markForCheck. They don't understand async pipe auto-triggers detection.

NgZone is a wrapper around Zone.js that allows you to run code inside or outside of Angular's change detection zone. Code running inside the zone triggers change detection automatically. Using runOutsideAngular() prevents unnecessary change detection for operations like animations or scroll handlers. Use run() to bring code back inside the zone when you need to update the view.
constructor(private ngZone: NgZone) {}

startAnimation() {
  this.ngZone.runOutsideAngular(() => {
    // This won't trigger change detection
    requestAnimationFrame(() => this.animate());
  });
}

updateUI(value: string) {
  this.ngZone.run(() => {
    this.result = value; // Triggers change detection
  });
}
runOutsideAngular() improves performance for frequent events. run() is needed to bring updates back into Angular's awareness when you want the DOM to reflect changes.

Why it matters: Tests understanding of NgZone for performance. Shows you know fine-grained control over change detection.

Real applications: Mouse tracking 60+ times per second outside Angular, scroll handlers without triggering detection, animations running independently.

Common mistakes: Developers don't use runOutsideAngular creating 1000+ detection cycles per second. They attach handlers inside Angular harming performance. They forget ngZone.run() to bring results back to view.

Angular is moving towards zoneless change detection where Zone.js is no longer needed. With signals and the new change detection model, Angular can track exactly which parts of the UI need updating. This improves performance and reduces bundle size since Zone.js patches many browser APIs. Zoneless mode is experimental in Angular 17+ and uses signals for fine-grained updates.
// Experimental zoneless setup in Angular 17+
bootstrapApplication(AppComponent, {
  providers: [
    provideExperimentalZonelessChangeDetection()
  ]
});

// Components use signals for reactive state
@Component({ template: '<p>{{ count() }}</p>' })
export class CounterComponent {
  count = signal(0);
  increment() { this.count.update(v => v + 1); }
}
Signals tell Angular exactly which components need re-rendering, eliminating the need for zone-based dirty checking. This is the future direction of Angular's reactivity model.

Why it matters: Tests understanding of Angular's evolution. Shows you know the future of change detection strategy.

Real applications: Angular 17+ projects use signals for performance, components with infrequent changes use fine-grained updates, bundle sizes reduce without Zone.js.

Common mistakes: Developers think NgModules and Zone.js are permanent. They don't adopt signals in new projects. They don't understand zoneless reduces bundle size significantly.