One question keeps surfacing on StackOverflow: why does the ngDoCheck lifecycle hook fire for a component that uses the OnPush change detection strategy? Typically, the question is phrased something like this:

I have used OnPush strategy for my component and no bindings have changed, but the ngDoCheck lifecycle hook is still triggered. Is the strategy not working?

This is a valid question, though it stems from a common misconception about when ngDoCheck actually runs and why the framework exposes this hook at all. This article clarifies the situation by explaining exactly when ngDoCheck is invoked and what purpose it serves.

When does ngDoCheck fire?

The official documentation provides only a brief description:

Detect and act upon changes that Angular can’t or won’t detect on its own.
Called during every change detection run, immediately after ngOnChanges() and ngOnInit().

From this we learn that it runs right after ngOnChanges and ngOnInit. But does that mean the component itself is being checked at that moment? To answer that, we first need to define what “checking” a component actually involves. A highly detailed explanation can be found in Everything you need to know about change detection in Angular, which highlights three core operations that occur during component change detection:

Lifecycle hooks are also triggered as part of the change detection process. Interestingly, the hooks for a child component are invoked while its parent is being checked. Let’s illustrate this with a simple component tree:

ComponentA
    ComponentB
        ComponentC

When Angular runs change detection, the sequence of operations looks like this:

Checking A component:
  - update B input bindings
  - call NgDoCheck on the B component
  - update DOM interpolations for component A
 
 Checking B component:
    - update C input bindings
    - call NgDoCheck on the C component
    - update DOM interpolations for component B
 
   Checking C component:
      - update DOM interpolations for component C

This list is a bit simplified compared to the full change detection flow described in the linked article, but it’s enough to demonstrate when ngDoCheck is called.

Notice that ngDoCheck on the child component B is triggered when the parent component is being checked. Now, what happens if we switch B to use the OnPush strategy? The flow changes as follows:

Checking A component:
  - update B input bindings
  - call NgDoCheck on the B component
  - update DOM interpolations for component A
  
 if (bindings changed) -> checking B component:
    - update C input bindings
    - call NgDoCheck on the C component
    - update DOM interpolations for component B
 
   Checking C component:
      - update DOM interpolations for component C

With OnPush in play, a condition—if (bindings changed) -> checking B component—is inserted before the check for B. If this condition fails, Angular skips the operations under checking B component. However, ngDoCheck on B will still fire, even though B isn’t actually being checked. It’s also important to note that this hook runs only for the top-level B component that has the OnPush strategy; its children, like C, will not have the hook triggered in this scenario.

So, the answer to the question:

I have used OnPush strategy for my component, but the ngDoCheck lifecycle hook is still triggered. Is the strategy not working?

is that the strategy is working as intended. The hook fires by design, and the next section explains why.

Why was ngDoCheck introduced?

Angular tracks input bindings by reference. If an object reference remains the same, Angular doesn’t detect a binding change and consequently skips change detection for any OnPush component. This is similar to the default behavior in AngularJS, where changes to an object like o also go undetected:

const o = {some: 3};

$scope.$watch(
  () => {  return o;},
  () => {  console.log('changed'); } // nothing is logged
);

$timeout(() => {  o.some = 4; }, 2000);

However, AngularJS offered additional watch options to catch object and array mutations—deep watch and collection watch. For deep watch, you had to pass true as the third argument to the $watch function:

$scope.$watch(
  () => {  return o;},
  () => {  console.log('changed'); }, // logs `changed`
  true
);

For watching collections, there was the dedicated $watchCollection method:

const o = [3];

$scope.$watchCollection(
  () => {  return o;},
  () => {  console.log('changed'); } // logs `changed`
);

$timeout(() => {  o.push(4) }, 2000);

Angular doesn’t have a direct equivalent. If you need to track mutations to an object or array, you have to do it manually. Once you detect a change, you must inform Angular so it can run change detection even if the object reference hasn’t changed.

Let’s adapt the AngularJS example to an Angular app. We have component A that uses the OnPush strategy and receives object o via an input binding. Its template references the name property:

@Component({
  selector: 'a-comp',
  template: `<h2>The name is: {{o.name}}</h2>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AComponent {
  @Input() o;
}

There’s also a parent App component that passes o down to the child a-comp. After two seconds, it mutates the object by updating the name and id properties:

@Component({
  selector: 'my-app',
  template: `
    <h1>Hello {{name}}</h1>
    <a-comp [o]="o"></a-comp>
  `,
})
export class App {
  name = `Angular! v${VERSION.full}`;
  o = {id: 1, name: 'John'};

  ngOnInit() {
    setTimeout(() => {
      this.o.id = 2;
      this.o.name = 'Jane';
    }, 2000);
  }
}

Because Angular relies on reference equality, mutating the object without creating a new reference goes unnoticed, and change detection is not triggered for A. As a result, the updated name value won’t be reflected in the DOM.

Fortunately, ngDoCheck offers a way out. We can use this hook to manually inspect the object for mutations and then call markForCheck to notify Angular. In the following implementation, we only track changes to the id property, but a full-fledged deep watch, much like the one in AngularJS, can be implemented if needed.

Let’s take a look:

export class AComponent {
  @Input() o;

  // store previous value of `id`
  id;

  constructor(private cd: ChangeDetectorRef) {}

  ngOnChanges() {
    // every time the object changes 
    // store the new `id`
    this.id = this.o.id;
  }

  ngDoCheck() {
    // check for object mutation
    if (this.id !== this.o.id) {
      this.cd.markForCheck();
    }
  }
}

This plunker demonstrates the approach in action. For a hands-on exercise, try implementing your own deep watch and collection watch utilities, similar to AngularJS.

One word of caution: the Angular team recommends using immutable objects to avoid the need for manual mutation tracking, allowing the default binding change mechanism to work. But since immutable objects aren’t always feasible, ngDoCheck exists as a practical fallback option.