Replace Zone.js Change Detection with Angular Signals for a Counter Component
Learn how to swap Angular’s default Zone.js change detection with Signals, a granular reactive state system. Follow our step‑by‑step guide to create signals, computed values, and effects, verify instant UI updates, and roll back if needed.
30 May 2026, 10:39 UTC

Desired Outcome
Swap the default Zone.js‑based change detection in an Angular component for the newer Signals API, so that only the component holding the signal is marked for check when its state changes. The UI should update instantly after calling .set() or .update() on a signal, without triggering a full application check.
Prerequisites
- Angular 17 (or later) project with
@angular/core@17+. - Basic understanding of Angular components and templates.
- Node.js 20+ and npm or yarn installed.
- Angular CLI installed globally (optional but recommended).
Step 1: Create a Component Using Signals
Generate a simple counter component:
ng generate component counter
In counter.component.ts, replace the class body with signal definitions:
import { Component, signal, computed, effect, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-counter',
templateUrl: './counter.component.html',
styleUrls: ['./counter.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
// Writable signal for the counter value
count = signal(0);
// Computed signal that doubles the counter
double = computed(() => this.count() * 2);
// Effect that logs the counter whenever it changes
constructor() {
effect(() => {
console.log('Counter updated:', this.count());
});
}
increment() {
this.count.update(n => n + 1);
}
decrement() {
this.count.update(n => n - 1);
}
}
Key points:
signal()creates a writable signal. The value is accessed by calling the signal as a function.computed()derives a new signal from other signals. It automatically re‑evaluates when dependencies change.effect()runs a function whenever any signal it reads changes. Use it sparingly for side effects.- Setting
ChangeDetectionStrategy.OnPushensures Angular only checks this component when its inputs change or when a signal it reads updates.
Step 2: Update the Template
Use the signals directly in the template. Angular automatically unwraps signal values when used in interpolation or property bindings.
<div>
<h3>Counter: {{ count() }}</h3>
<h4>Double: {{ double() }}</h4>
<button (click)="increment()">Increment</button>
<button (click)="decrement()">Decrement</button>
</div>
Step 3: Verify Instant UI Updates
Run the application:
ng serve
Open http://localhost:4200 and click the buttons. The numbers should change immediately without a noticeable delay.
To confirm that only this component is marked for check:
- Open Angular DevTools (Chrome extension).
- Navigate to the Component Tree panel.
- Click the
CounterComponentnode. When you click a button, the component’s status should change toPendingand then toCheckedquickly. - Other components in the tree should remain
Unchanged.
Additionally, check the browser console. The effect should log the counter value each time you click a button, confirming that the signal change triggered the effect.
Step 4: Handling Asynchronous Data (Optional)
Signals are synchronous by design. If you need to handle async streams, combine signals with fromEvent or RxJS observables, but keep the actual state signal synchronous. Example:
import { fromEvent } from 'rxjs';
import { map } from 'rxjs/operators';
const click$ = fromEvent(document, 'click').pipe(map(() => 1));
click$.subscribe(() => this.count.update(n => n + 1));
Step 5: Rollback or Recovery
If you encounter issues such as infinite loops or side‑effect errors, revert to the previous state by:
- Removing the
signal(),computed(), andeffect()calls. - Restoring the original property and method definitions:
- Re‑setting the change detection strategy to
ChangeDetectionStrategy.Defaultor removing it entirely.
count = 0;
increment() { this.count++; }
Do not delete the ChangeDetectionStrategy.OnPush flag until you confirm the component works with the fallback implementation.
Limitations and Best Practices
- Signals are best for synchronous state. Use RxJS for asynchronous streams.
- Avoid mutating signals inside a
computed()to prevent infinite loops. - Effects should be used for side effects like logging, analytics, or UI side‑effects, not for primary state changes.
- When integrating with third‑party libraries that rely on Zone.js, ensure they are compatible with
OnPushand signal updates.
Conclusion
By replacing Zone.js change detection with Angular Signals, you achieve fine‑grained, efficient UI updates. The example above demonstrates a minimal counter component that updates instantly, logs changes, and limits re‑rendering to the component itself. Verify the behavior with Angular DevTools, and roll back to the traditional approach if you run into compatibility or complexity issues.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.