Tutorials Logic, IN info@tutorialslogic.com

Angular Change Detection OnPush Signals

Change Detection Model

Change detection synchronizes reactive application state with rendered views. Learn Default and OnPush behavior, signal notifications, zoneless requirements, and the limited cases where manual ChangeDetectorRef control is appropriate.

Change detection evaluates template bindings and updates the DOM when Angular receives a notification that visible state may have changed. Zoneless scheduling is the default in Angular v21 and later; applications can still use signals, inputs, events, the async pipe, and explicit change-detection notifications.

During a check, Angular evaluates template bindings and updates DOM values whose results changed. It traverses the component tree once, so changing already-checked state during lifecycle hooks can produce ExpressionChangedAfterItHasBeenCheckedError in development.

Signals provide precise notifications when a template reads them. Template events, input assignments, async-pipe emissions, attached dirty views, and explicit change-detection APIs are other notifications Angular can use.

OnPush and Eager Strategies

OnPush is the default strategy for new components in Angular v22. It lets Angular skip a clean subtree until a relevant notification marks it. Eager checks a component whenever traversal reaches it; ChangeDetectionStrategy.Default remains an alias for Eager and is deprecated.

OnPush is not manual rendering. Angular still checks after a changed bound input, an event handled in the subtree, a signal read by the template changes, an async-pipe emission, view attachment, or markForCheck. The notification contract matters more than the strategy label.

Strategy When it checks Best for
OnPush When the subtree receives a supported notification Default choice and predictable state ownership
Eager Whenever change-detection traversal reaches the component Compatibility code that intentionally relies on eager checking
Default Alias of Eager Deprecated name retained for source compatibility

OnPush Updates

When a parent mutates a property inside the same input object, the child receives no new input reference. Replace the object or collection so the ownership change is explicit, or place mutable state behind a signal the child reads.

Do not call detectChanges after every assignment to compensate for unclear state flow. First identify who owns the state and which normal notification should mark the view.

OnPush Strategy

OnPush Strategy
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';

interface Product {
    id: number;
    name: string;
    price: number;
}

@Component({
    selector: 'app-product',
    standalone: true,
    changeDetection: ChangeDetectionStrategy.OnPush,
    template: `
        <div>
            <h3>{{ product.name }}</h3>
            <p>${{ product.price }}</p>
        </div>
    `
})
export class ProductComponent {
    @Input() product!: Product;
    // Angular checks after a supported notification: an input update,
    // a handled event, a consumed signal or AsyncPipe emission,
    // view attachment, or markForCheck().
}

// IMPORTANT: With OnPush, mutating the object won't trigger re-render
// BAD:  this.product.price = 20;  // same reference - no update
// GOOD: this.product = { ...this.product, price: 20 };  // new reference

Zoneless Updates

When a template reads a signal, Angular tracks that dependency and marks the consuming view when the signal changes. In a zoneless application, signals are one of several supported notifications and do not require Zone.js to schedule the update.

Zoneless does not mean signals are mandatory everywhere. Template listeners, input updates, async pipe, setInput, markForCheck, and attaching a dirty view can all notify Angular. A callback from a third-party API must eventually perform one of these notifications when it changes visible state.

Test loading, error, timer, overlay, and third-party integration paths during migration. Code that appeared to work only because Zone.js scheduled a broad check needs an explicit state notification.

  • Reactive form model mutations do not by themselves schedule a zoneless view update; connect the relevant status or value stream to a signal or call markForCheck.
  • Use PendingTasks or pendingUntilEvent when custom asynchronous work must delay server-render serialization.
  • Use afterNextRender or afterEveryRender instead of NgZone stability events when work must wait for rendering.

Zoneless with Signals

Zoneless with Signals
import { Component, signal, computed } from '@angular/core';

@Component({
    selector: 'app-counter',
    standalone: true,
    template: `
        <p>Count: {{ count() }}</p>
        <p>Doubled: {{ doubled() }}</p>
        <button (click)="increment()">+1</button>
    `
})
export class CounterComponent {
    count   = signal(0);
    doubled = computed(() => this.count() * 2);

    increment() {
        this.count.update(v => v + 1);
    }
    // Angular only re-renders this component when count() changes
    // No Zone.js needed - pure signal-based reactivity
}

Match Zoneless Production Behavior in TestBed

Match Zoneless Production Behavior in TestBed
import { provideZonelessChangeDetection } from '@angular/core';
import { TestBed } from '@angular/core/testing';

TestBed.configureTestingModule({
  // Useful when zone.js is still loaded by the test polyfills.
  providers: [provideZonelessChangeDetection()]
});

const fixture = TestBed.createComponent(StatusComponent);
fixture.componentInstance.refresh();
await fixture.whenStable();
expect(fixture.nativeElement.textContent).toContain('Ready');

Manual Change Detection

markForCheck marks an OnPush view and its ancestors for a future check. detectChanges immediately checks a view and descendants. detach removes a view from normal traversal until reattach; a detached view can still be checked explicitly.

Manual APIs are appropriate for measured high-frequency or externally scheduled integrations, not as the first fix for stale UI. Document the scheduling contract and add a test that proves when the view updates.

ChangeDetectorRef

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

@Component({
    selector: 'app-manual',
    standalone: true,
    changeDetection: ChangeDetectionStrategy.OnPush,
    template: `<p>{{ data }}</p>`
})
export class ManualComponent {
    private cdr = inject(ChangeDetectorRef);
    data = 'initial';

    updateFromExternalSource() {
        // Called from a third-party library callback (outside Angular)
        this.data = 'updated';
        this.cdr.markForCheck();   // tell Angular to check this component
    }

    pauseDetection() {
        this.cdr.detach();         // stop checking this component
    }

    resumeDetection() {
        this.cdr.reattach();       // resume checking
        this.cdr.detectChanges();  // run one check immediately
    }
}
Confirm the page outcome

Rendering Review

5 checks
  • I can name the notifications Angular uses to schedule zoneless updates.
  • I can explain when OnPush and Eager views are checked and why Default is now only a deprecated Eager alias.
  • I replace mutated input objects when a child depends on reference changes.
  • I use markForCheck, detectChanges, detach, and reattach only for a documented scheduling need.
  • I test timers, third-party callbacks, overlays, and async states while migrating away from Zone.js.

Rendering Review Questions

The parent mutated product.price on the same object. OnPush input checking uses reference changes as an important notification, so the child may not be checked for that mutation.

Without Zone.js, Angular does not treat every asynchronous callback as a reason to check the application. A signal write, template event, input update, async-pipe emission, or explicit change-detection notification tells Angular that the view may need work.

detach suits views that update on their own schedule, such as a high-frequency telemetry panel. The component must call detectChanges to refresh; ordinary OnPush components usually only need markForCheck.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.