Interview answer drill

Use this Angular interview question to rehearse a quick answer, common mistake, follow-up, and production pitfall.

What are change detection strategies in Angular, and how do they work?Frontend interview answer

HighIntermediateAngular
Interview focus

This Angular interview question tests whether you can explain Angular change detection strategies in production: Default vs OnPush, triggers, and stale-UI bugs, connect it to production trade-offs, and handle common follow-up questions.

  • Angular change detection strategies in production: Default vs OnPush, triggers, and stale-UI bugs explanation without falling back to memorized docs wording
  • Change Detection and Performance reasoning, edge cases, and production failure modes
  • How you would answer the most likely Angular interview follow-up
Practice more Angular interview questions
Interview quick answer

Angular change detection strategy is a production performance and debugging choice, not just a framework definition. The useful answer compares Default versus OnPush through stale-UI bugs, trigger rules, immutable updates, and when to use markForCheck() or detectChanges().

Full interview answer

Production decision first

The real question is not “what is change detection?” but when should this subtree be checked. Default is the safe baseline when you want fewer stale-UI surprises. OnPush is the performance strategy when you want predictable checks, immutable inputs, and fewer wasted passes through a large component tree.

How Angular thinks about it
Angular keeps a tree of views. On each detection pass it decides which branches need checking, evaluates bindings, and updates the DOM if values changed. That is why strategy choice matters more in large trees than in tiny demo components.

Two strategies, two trade-offs
Interviewers usually want the architecture trade-off, not just the names:

Strategy

What Angular does

Where it wins

Common production bug

ChangeDetectionStrategy.Default

Checks the component whenever Angular runs a normal change-detection pass for that branch.

Simpler state model and fewer stale-view surprises.

Large trees rerender too often because everything stays eligible for checks.

ChangeDetectionStrategy.OnPush

Checks mainly when an input reference changes, an event happens in the subtree, an async pipe emits, or you mark the view manually.

Large lists, reactive UIs, and predictable immutable state flows.

UI looks stale because code mutated an object or updated state in a manual subscription without marking the view.

Default vs OnPush in real apps
TYPESCRIPT
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';

@Component({
  selector: 'app-user-card',
  template: `<p>{{ user.name }}</p>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserCardComponent {
  @Input() user!: { name: string };
}
                  

This child is OnPush, so Angular cares about reference changes and explicit triggers, not deep mutation. If a parent does user.name = 'Bob' on the same object, the child can stay stale even though the data technically changed.

TYPESCRIPT
@Component({
  selector: 'app-parent',
  template: `
    <app-user-card [user]="user"></app-user-card>
    <button (click)="renameWrong()">Wrong</button>
    <button (click)="renameCorrect()">Correct</button>
  `
})
export class ParentComponent {
  user = { name: 'Alice' };

  // ❌ Same object reference: an OnPush child can stay stale
  renameWrong() {
    this.user.name = 'Bob';
  }

  // ✅ New reference: OnPush sees a meaningful input change
  renameCorrect() {
    this.user = { ...this.user, name: 'Bob' };
  }
}
                  

Why OnPush still updates after events and async work
OnPush does not mean “never update unless an input changes.” Events inside the subtree, async pipe emissions, and manual CDR calls still mark the view dirty. That is why a stale card sometimes “wakes up” after a click or Observable emission.

Trigger

Default

OnPush

Why it matters

Parent mutates existing object

✅ Usually visible on next pass

❌ Often skipped

OnPush compares input references, not deep object state.

Parent passes a new input reference

✅ Yes

✅ Yes

A new reference is a meaningful input change.

Event happens inside the component subtree

✅ Yes

✅ Yes

Angular marks that branch dirty.

Observable emits through async pipe

✅ Yes

✅ Yes

AsyncPipe internally marks the view for checking.

markForCheck()

✅ Yes

✅ Yes

Schedules the branch for the next normal pass.

detectChanges()

✅ Yes

✅ Yes

Forces an immediate synchronous check on this subtree.

Compact trigger matrix

markForCheck() vs detectChanges()

markForCheck() says “include me in the next normal pass.” detectChanges() says “run my subtree right now.” If you reach for detectChanges() by default, you usually have a lifecycle-timing smell rather than a true need for synchronous rendering.

TYPESCRIPT
import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core';

@Component({
  selector: 'app-live-count',
  template: `{{ total }}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class LiveCountComponent {
  total = 0;

  constructor(private cd: ChangeDetectorRef) {}

  connectToExternalCallback(source: { onMessage(cb: (n: number) => void): void }) {
    source.onMessage(n => {
      this.total = n;
      this.cd.markForCheck();
    });
  }

  flushImmediatelyAfterViewMutation() {
    this.total++;
    this.cd.detectChanges();
  }
}
                  

Follow-up confusion that comes up in real debugging

ViewChild or direct child mutation: if you grab a child instance and mutate state directly, Angular may not treat that as a meaningful OnPush input change, so you often need a new reference or markForCheck().

ExpressionChangedAfterItHasBeenCheckedError: this usually means you changed bound state during the same detection turn after Angular already checked it. The fix is usually to move the update earlier, schedule it to the next turn, or rethink the data flow instead of blindly calling detectChanges() everywhere.

Profiling question

Check first

Why

Why is this list rerendering too often?

Angular DevTools and whether child subtrees should be OnPush

You want evidence before changing strategy.

Why does this OnPush child stay stale?

Look for in-place mutation or manual subscriptions without markForCheck()

Those are the two most common real bugs.

Why did this expression-changed error appear?

Check lifecycle timing and state writes after Angular already checked the view

The problem is often timing, not missing brute-force detection.

Profiling-first debug checklist
Summary
  • Default favors simplicity; OnPush favors predictable performance in large trees.
  • OnPush still updates on events, async pipe emissions, and manual CDR APIs, but it will skip deep mutation on the same input reference.
  • markForCheck() is the normal manual escape hatch; detectChanges() is the sharper synchronous tool.
  • The highest-signal production bugs are stale OnPush UIs from mutation, manual subscription updates without marking, and lifecycle-timing errors like ExpressionChangedAfterItHasBeenCheckedError.
Similar questions
Guides
Preparing for interviews?

Use this as one explanation rep, then continue with the Angular interview questions cluster or a guided prep path.