Angular Signals Explained — Reactive State Management Made Simple
In this tutorial, you will learn about Angular Signals Explained. We cover key concepts, practical examples, and best practices to help you master this topic.
Angular signals are reactive primitives that hold values and notify consumers when those values change, providing a simpler and more performant alternative to RxJS for state management.
What You'll Learn
- What signals are and how they differ from RxJS observables
- How to create and update writable signals
- How to derive values with computed signals
- How to use effects for side effects
- How signals integrate with change detection
Why It Matters
Signals simplify Reactive Programming in Angular. They eliminate the need for Zone.js for state changes, provide fine-grained reactivity, and make change detection more efficient by tracking exactly which parts of the template depend on which signals.
Real-World Use
Durga Antivirus Pro's scan progress indicator uses signals. The scan percentage, file count, and threat count are signals updated by the scan engine. The template reads these signals directly, and Angular updates only the specific DOM elements that changed.
flowchart LR
A[Writable Signal] -->|set/update| B[Value]
B -->|derived from| C[Computed Signal]
B -->|side effect| D[Effect]
C --> E[Template]
B --> E
D --> F[Console / API / Storage]
style A fill:#f97316,color:#fff
Creating and Using Signals
Create a writable signal with signal():
import { Component, signal } from "@angular/core";
@Component({
selector: "app-counter",
standalone: true,
template: `
<p>Count: {{ count() }}</p>
<button (click)="increment()">+</button>
<button (click)="decrement()">-</button>
<button (click)="reset()">Reset</button>
`
})
export class CounterComponent {
count = signal(0);
increment() {
this.count.update(c => c + 1);
}
decrement() {
this.count.update(c => c - 1);
}
reset() {
this.count.set(0);
}
}
Expected output: Buttons that increment, decrement, and reset the count. The displayed value updates immediately without any change detection configuration.
A signal is a function that returns its current value. count() reads the value. set() replaces it. update() derives a new value from the current one. Angular tracks signal reads in the template and only updates the DOM when those specific signals change.
Computed Signals
Derive values from other signals using computed():
import { Component, signal, computed } from "@angular/core";
@Component({
selector: "app-cart",
standalone: true,
template: `
<ul>
<li *ngFor="let item of items(); trackBy: trackByFn">
{{ item.name }} - \${{ item.price }}
<button (click)="removeItem(item.id)">Remove</button>
</li>
</ul>
<p>Subtotal: \${{ subtotal() }}</p>
<p>Tax (10%): \${{ tax() }}</p>
<p><strong>Total: \${{ total() }}</strong></p>
<p>Item count: {{ itemCount() }}</p>
`
})
export class CartComponent {
items = signal<{ id: number; name: string; price: number }[]>([]);
itemCount = computed(() => this.items().length);
subtotal = computed(() => this.items().reduce((sum, item) => sum + item.price, 0));
tax = computed(() => this.subtotal() * 0.1);
total = computed(() => this.subtotal() + this.tax());
trackByFn = (_index: number, item: { id: number }) => item.id;
addItem(name: string, price: number) {
this.items.update(items => [...items, { id: Date.now(), name, price }]);
}
removeItem(id: number) {
this.items.update(items => items.filter(item => item.id !== id));
}
}
Expected output: A cart that shows items, calculates subtotal, tax, and total automatically. When items change, all computed values update.
A computed signal lazily evaluates its derivation function and caches the result. It only re-evaluates when its dependent signals change. This is more efficient than calling a method in the template.
Effects
Use effect() for side effects like logging or saving to storage:
import { Component, signal, effect, OnInit, inject } from "@angular/core";
@Component({
selector: "app-theme-switcher",
standalone: true,
template: `
<button (click)="toggleTheme()">
Switch to {{ theme() === "light" ? "dark" : "light" }}
</button>
`
})
export class ThemeSwitcherComponent implements OnInit {
theme = signal(localStorage.getItem("theme") || "light");
constructor() {
effect(() => {
const current = this.theme();
document.body.setAttribute("data-theme", current);
localStorage.setItem("theme", current);
console.log("Theme changed to:", current);
});
}
toggleTheme() {
this.theme.update(t => t === "light" ? "dark" : "light");
}
}
Expected output: Clicking the button toggles the theme, updates the body attribute, saves to localStorage, and logs the change. The effect runs whenever theme() changes.
Effects run when any signal read inside them changes. They run once by default during creation and again on subsequent changes. Effects do not trigger change detection themselves, making them ideal for offloading work that does not affect the UI directly.
Signals vs RxJS
Signals and RxJS serve different purposes:
import { Component, signal, OnInit, OnDestroy } from "@angular/core";
import { Observable, interval, Subscription } from "rxjs";
import { toObservable, toSignal } from "@angular/core/rxjs-interop";
@Component({
selector: "app-signal-vs-rxjs",
standalone: true,
template: `
<p>Signal count: {{ signalCount() }}</p>
<p>Observable value (via signal): {{ rxjsSignal() }}</p>
`
})
export class SignalVsRxjsComponent implements OnInit, OnDestroy {
// Signal-based
signalCount = signal(0);
private sub?: Subscription;
// Convert RxJS to signal
rxjsSignal = toSignal(interval(1000), { initialValue: 0 });
constructor() {
effect(() => {
console.log("Signal count changed:", this.signalCount());
});
}
ngOnInit() {
this.sub = interval(2000).subscribe(() => {
this.signalCount.update(c => c + 1);
});
}
ngOnDestroy() {
this.sub?.unsubscribe();
}
}
Expected output: The signal count increments every 2 seconds via RxJS subscription. The rxjsSignal updates every 1 second.
Use signals for synchronous state management. Use RxJS for event streams, debouncing, and complex async composition. toSignal and toObservable bridge both worlds.
Signal Inputs (Angular 17.1+)
Signal-based inputs replace @Input():
import { Component, input, output } from "@angular/core";
@Component({
selector: "app-user-card",
standalone: true,
template: `
<div class="card">
<h3>{{ name() }}</h3>
<p>{{ email() }}</p>
<button (click)="select.emit({ name: name(), email: email() })">Select</button>
</div>
`
})
export class UserCardComponent {
name = input.required<string>();
email = input("no-email@example.com");
select = output<{ name: string; email: string }>();
}
Expected output: A user card component that receives required and optional inputs as signals and emits typed events.
Signal inputs are read-only signals. input.required() makes the input required. input(defaultValue) provides a default. output() replaces @Output with EventEmitter.
Common Mistakes
Calling signal setter inside computed — Computed must be pure. Calling
signal.set()orsignal.update()insidecomputed()throws an error.Forgetting to call signal as function —
countis the signal function.count()returns the value. Usingcountin the template renders nothing.Creating signals for static values — If a value never changes, use a regular property. Signals add unnecessary overhead.
Mutating signal state directly —
mySignal().push(item)does not trigger updates. Always useset()orupdate()with a new reference.Using signals for everything — Signals excel at state management. For event streams or HTTP requests, RxJS remains the better choice.
Practice Questions
How do you create a writable signal?
const count = signal(0);creates a signal with initial value 0.What is the difference between
setandupdate?set(newValue)replaces the value.update(fn)derives new value from the current value.When does a computed signal re-evaluate? Lazily, when its value is read and its dependencies have changed since the last read.
What happens when a signal changes in the template? Angular updates only the DOM nodes that depend on that specific signal, not the entire component.
How do you create an effect for side effects? Use
effect(() => { ... })inside a constructor or injection context.
Challenge
Build a ShoppingCartComponent using only signals (no RxJS). The cart holds items with name, price, and quantity. Use computed signals for subtotal, tax, total, and item count. Add an effect that saves the cart to localStorage on every change. Use signal inputs for item data and signal outputs for checkout events.
FAQ
Mini Project
Build a VotingPollComponent using signals. The poll has a question and options with vote counts. Use a signal for the selected option, computed for the total votes and percentages, and an effect to log when the leading option changes. Use signal inputs for the poll data and signal output for the vote submission. Display a bar chart using CSS that updates reactively.
What's Next
Continue with advanced component patterns:
Angular Content Projection, Angular Dynamic Components, Angular Standalone
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro