Key Takeaways
ngDoCheck()lets you implement custom comparisons; it doesn't automatically inspect nested properties or request a view update.- An OnPush child's hook can run when its containing parent view is checked, even while Angular skips the child's own template.
- For object inputs, prefer immutable replacement. If mutation is unavoidable, explicitly notify Angular with a mechanism such as
markForCheck(). - Keep custom checks narrow: their cost is paid whenever the hook runs, not just when your data changes.
Angular's ngDoCheck() is a lifecycle hook for custom checks after Angular evaluates a component's input bindings in its containing view. This Angular ngDoCheck example demonstrates an important distinction: the hook can execute without Angular refreshing an OnPush child's template. The hook itself doesn't schedule that refresh.
When Angular Calls ngDoCheck
For a child declared in a parent template, distinguish checking the child's input bindings from checking the child's own view—the template that renders its HTML. Angular's DoCheck API documentation explicitly says the hook can execute even when the component's own change detection is skipped.
During initialization, the first ngDoCheck() follows ngOnInit(). See our Angular ngOnInit example for initialization-specific work. Don't interpret ngDoCheck() as a callback for every application event: if Angular doesn't check the containing view, that child's hook won't run through it.
Angular ngDoCheck Example With OnPush
This example targets Angular 22 standalone components. In an existing Angular CLI browser project, replace src/main.ts with the following file and keep <app-root></app-root> in src/index.html. The sample includes both components and the standalone bootstrap; it explicitly selects OnPush and enables zoneless change detection.
The child uses a typed, required input. The decorator-based @Input API remains supported; required: true makes a missing template binding a build-time error.
// src/main.ts
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DoCheck,
Input,
inject,
provideZonelessChangeDetection,
} from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
interface User {
name: string;
}
@Component({
selector: 'app-child',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<p>Here is the user name: {{ user.name }}</p>`,
})
export class ChildComponent implements DoCheck {
@Input({ required: true }) user!: User;
private readonly cdr = inject(ChangeDetectorRef);
ngDoCheck(): void {
console.log('DO CHECK:', this.user.name);
}
requestCheck(): void {
this.cdr.markForCheck();
}
}
@Component({
selector: 'app-root',
standalone: true,
imports: [ChildComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<button type="button" (click)="mutateUser()">
Mutate in place
</button>
<button type="button" (click)="replaceUser()">
Replace object
</button>
<button type="button" (click)="mutateAndNotify(child)">
Mutate and notify
</button>
<p>Parent sees: {{ user.name }}</p>
<app-child #child [user]="user"></app-child>
`,
})
export class ParentComponent {
user: User = { name: 'Alex' };
mutateUser(): void {
this.user.name = 'ted';
}
replaceUser(): void {
this.user = { ...this.user, name: 'ted' };
}
mutateAndNotify(child: ChildComponent): void {
this.mutateUser();
child.requestCheck();
}
}
bootstrapApplication(ParentComponent, {
providers: [provideZonelessChangeDetection()],
}).catch((error: unknown) => {
console.error('Bootstrap failed', error);
});Compare the Three Update Paths
Reload before testing each button so both components start with Alex. The following results describe rendering after the button event, not the initial render. They follow from Angular's documented OnPush subtree rules and its hook-execution tests.
| Button | Input object identity | Child ngDoCheck | Child text |
|---|---|---|---|
| Mutate in place | Unchanged | Runs | Alex |
| Replace object | New object | Runs | ted |
| Mutate and notify | Unchanged | Runs | ted |
For the mutation-only button, ngDoCheck() logs ted while the child still displays Alex. Parent and child share the same object, so the child's JavaScript property already contains the new name; its rendered HTML is stale.
Parent button event
-> Parent template and child's input binding checked
-> Child ngDoCheck runs
|
+-- Same reference, no child notification -> Child template skipped
|
+-- New reference or child marked --------> Child template checkedPrefer Immutable Input Replacement
replaceUser() creates a new object rather than changing the existing one. When Angular evaluates [user]="user", the binding now has a different object identity, making the OnPush child eligible for checking. No custom comparison is needed for this update.
The mutation-only button is handled in the parent, outside the child's subtree. That event makes the parent eligible for checking, but doesn't by itself mark the unchanged-input child. An event handled inside the child's subtree is a different trigger. OnPush is not an input-only strategy.
Explicitly Notify Angular When Mutation Is Unavoidable
mutateAndNotify() keeps the object identity but calls the child's requestCheck() method. That method uses the child's own ChangeDetectorRef to mark its view for checking. Marking only the parent wouldn't automatically mark every OnPush descendant.
markForCheck() marks a view for a subsequent check; it isn't a synchronous render command. detectChanges() performs an explicit local check instead. Prefer notification at the update boundary rather than forcing checks throughout your component tree.
ngDoCheck vs ngOnChanges
ngOnChanges() responds to changes Angular detects in input binding values. For an object input, changing user.name doesn't change the bound object identity, so that mutation alone doesn't produce an input change.
If you added ngOnChanges() to this child, replacing the object would invoke it; mutating the object and calling markForCheck() would not produce a new change for the user binding. Marking a view doesn't change its inputs. Our Angular ngOnChanges example covers input-change handling in more detail.
ngDoCheck() is an extension point for your own comparison. It doesn't receive an automatic deep diff. Choose one mechanism to handle a particular input's changes rather than duplicating the same production logic in both hooks.
When Should You Use ngDoCheck?
Consider a narrow compatibility case: a legacy integration supplies a mutable record, you can't change that contract, and you need to watch one known field. You can keep a snapshot of that field and compare it when the hook runs.
For that deliberate behavior, replace the child's logging hook above with this fragment and add the snapshot field. It uses the cdr already injected in the example:
// Fragment: replace ChildComponent above; keep its existing imports and User interface.
@Component({
selector: 'app-child',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<p>Here is the user name: {{ user.name }}</p>`,
})
export class ChildComponent implements DoCheck {
@Input({ required: true }) user!: User;
private readonly cdr = inject(ChangeDetectorRef);
private previousName: string | undefined;
ngDoCheck(): void {
const currentName = this.user.name;
if (currentName === this.previousName) {
return;
}
this.previousName = currentName;
this.cdr.markForCheck();
}
requestCheck(): void {
this.cdr.markForCheck();
}
}This intentionally changes the mutation-only button's behavior: the comparison notices the name change and explicitly marks the child. Comparing the field and notifying Angular are separate operations; implementing the hook alone does neither automatically. The snapshot is updated before marking, so an unchanged name doesn't repeatedly request checks.
Signals and Zoneless Change Detection
A signal is a value container that notifies its consumers when its value changes. When a child template reads a signal, Angular tracks that dependency and marks the child when the signal changes. Mutating an object inside a signal without updating the signal is not a notification; use an immutable value replacement through set() or update().
Zoneless change detection relies on supported notifications, including bound listeners, changed template-read signals, and markForCheck(). AsyncPipe also calls markForCheck() when it receives an update. Don't assume an arbitrary asynchronous mutation will schedule a check, or that ngDoCheck() supplies that scheduling.
Common Mistakes
- Equating hook logs with rendering. Inspect the DOM as well as the console. This example deliberately shows those outcomes diverging.
- Deep-comparing everything on every invocation. Traversing a whole object graph makes each comparison's work grow with the data visited. Compare the smallest relevant value instead.
- Leaving diagnostic work in a hot hook. Remove demonstration logging and avoid network requests or other expensive work in repeated checks.
- Hiding missing notifications in tests. Exercise the real update path and verify the rendered result. Forcing a check can conceal that production code never notified Angular.
Angular executes lifecycle hooks during change detection, so slow hook computations delay that work. The single-field example keeps comparison scope small; immutable inputs or explicit state notifications usually let you remove the hook entirely.
FAQ
When does Angular call ngDoCheck?
For a component declared in a parent template, Angular calls ngDoCheck after checking its input bindings in the containing view. Its first invocation follows ngOnInit, and it can execute even when the component's own OnPush template is skipped.
Does ngDoCheck run when an OnPush component is skipped?
It can: skipping the component's own template doesn't necessarily skip its ngDoCheck hook. If Angular checks the containing parent view, the hook can run; if that containing view is also skipped, the child's hook won't execute through it.
Does ngDoCheck detect nested object mutations automatically?
No. You must implement the comparison yourself, and detecting a difference doesn't automatically notify Angular to refresh an OnPush view.
What is the difference between ngDoCheck and ngOnChanges?
ngOnChanges responds to changes Angular detects in input binding values. ngDoCheck provides a place for custom comparisons, including comparisons of a property inside an unchanged object.
Should I use immutable updates or ngDoCheck with OnPush?
Prefer immutable input updates when you control the data flow. Use ngDoCheck for narrowly scoped compatibility checks, and keep the comparison separate from the notification needed to refresh the view.
Further Reading
- Angular DoCheck API — invocation timing and the skipped-view distinction.
- Skipping component subtrees — OnPush inputs, events, and mutation behavior.
- Component lifecycle — hook order and intended responsibilities.
- ChangeDetectorRef API — marking views versus explicitly checking them.
- Angular change-detection acceptance tests — executable specifications for OnPush and hook behavior.
- Angular signals — dependency tracking and explicit state updates.
- Zoneless change detection — notification mechanisms and testing guidance.
- Slow computations — diagnosing expensive template and lifecycle work.
