Signals are the biggest change to Angular's reactivity model since the framework launched. They let you manage component state without subscriptions, without async pipe, and without manually calling markForCheck(). Here's how they actually work.
The Problem Signals Solve
Before signals, reactive component state in Angular typically meant RxJS BehaviorSubject:
// Before signals — BehaviorSubject pattern
export class CartComponent {
private cartCount$ = new BehaviorSubject(0)
count$ = this.cartCount$.asObservable()
add() {
this.cartCount$.next(this.cartCount$.getValue() + 1)
}
}
<!-- template needed async pipe -->
<span>{{ count$ | async }}</span>
With signals:
// With signals
export class CartComponent {
count = signal(0)
add() {
this.count.update(c => c + 1)
}
}
<!-- template reads signal directly -->
<span>{{ count() }}</span>
No BehaviorSubject, no async pipe, no subscription management. The signal value is read by calling it like a function.
signal() — Creating Reactive State
import { Component, signal } from '@angular/core'
@Component({
selector: 'app-counter',
standalone: true,
template: `
<div class="card border-0 shadow-sm p-4 text-center">
<h2 class="display-4 fw-bold mb-3" style="color:#fd4766;">{{ count() }}</h2>
<div class="d-flex gap-2 justify-content-center">
<button class="btn btn-outline-secondary" (click)="decrement()">−</button>
<button class="btn text-white" style="background:#fd4766;" (click)="increment()">+</button>
<button class="btn btn-outline-secondary" (click)="reset()">Reset</button>
</div>
</div>
`
})
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) }
}
Three ways to update a signal:
// set() — replace value directly
this.count.set(10)
// update() — derive next value from current
this.count.update(current => current + 1)
// mutate() — for objects/arrays, mutate in place
this.items.mutate(arr => arr.push(newItem))
computed() — Derived State
computed() creates a read-only signal that automatically recalculates when its dependencies change:
import { Component, signal, computed } from '@angular/core'
@Component({
selector: 'app-cart',
standalone: true,
template: `
<div class="card border-0 shadow-sm p-4">
<div class="d-flex justify-content-between mb-3">
<span class="fw-bold">Items</span>
<span class="fw-bold" style="color:#fd4766;">{{ total() | currency }}</span>
</div>
<div class="d-flex align-items-center gap-3 mb-3">
<label class="form-label mb-0 small">Qty:</label>
<input type="number" class="form-control form-control-sm w-auto"
[value]="quantity()"
(change)="quantity.set(+$any($event.target).value)">
</div>
<p class="text-muted small mb-0">
{{ quantity() }} × {{ unitPrice() | currency }} = {{ total() | currency }}
</p>
<p class="text-muted small mb-0" *ngIf="hasDiscount()">
🎉 Discount applied: {{ discountedTotal() | currency }}
</p>
</div>
`
})
export class CartComponent {
quantity = signal(1)
unitPrice = signal(29.99)
// Recalculates whenever quantity or unitPrice changes
total = computed(() => this.quantity() * this.unitPrice())
// Chains computed signals
hasDiscount = computed(() => this.quantity() >= 3)
discountedTotal = computed(() => this.hasDiscount() ? this.total() * 0.9 : this.total())
}
computed() is lazy — it only recalculates when actually read, and only if a dependency changed. Angular caches the value between reads.
effect() — Side Effects
effect() runs whenever the signals it reads change:
import { Component, signal, effect } from '@angular/core'
@Component({
selector: 'app-theme',
standalone: true,
template: `
<div class="d-flex align-items-center gap-3">
<span class="small fw-semibold">Theme:</span>
<button class="btn btn-sm"
[class.btn-dark]="isDark()"
[class.btn-outline-secondary]="!isDark()"
(click)="isDark.update(d => !d)">
{{ isDark() ? '🌙 Dark' : '☀️ Light' }}
</button>
</div>
`
})
export class ThemeComponent {
isDark = signal(false)
constructor() {
// Runs immediately and whenever isDark changes
effect(() => {
document.documentElement.setAttribute(
'data-bs-theme',
this.isDark() ? 'dark' : 'light'
)
localStorage.setItem('theme', this.isDark() ? 'dark' : 'light')
})
}
}
effect() must be called in an injection context (constructor or field initializer). Don't write signals inside effects without allowSignalWrites: true — use update() in event handlers instead.
input() — Signal-Based Inputs
Angular 17.1+ introduced input() as a signal-based replacement for @Input():
import { Component, input, computed } from '@angular/core'
@Component({
selector: 'app-template-card',
standalone: true,
template: `
<div class="card border-0 shadow-sm h-100">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-start mb-2">
<h5 class="fw-bold mb-0">{{ name() }}</h5>
<span class="badge text-white" style="background:#fd4766;">
{{ framework() }}
</span>
</div>
<p class="text-muted small mb-3">{{ description() }}</p>
<div class="d-flex justify-content-between align-items-center">
<span class="fw-bold" style="color:#fd4766;">{{ displayPrice() }}</span>
<a [href]="'https://lettstartdesign.com'"
class="btn btn-sm text-white" style="background:#fd4766;"
target="_blank" rel="noopener">
Buy — FIRST30
</a>
</div>
</div>
</div>
`
})
export class TemplateCardComponent {
// Signal inputs — automatically reactive
name = input.required<string>()
description = input.required<string>()
framework = input<string>('Bootstrap')
price = input<number>(29)
// Computed from inputs — updates when inputs change
displayPrice = computed(() => `$${this.price()}`)
}
<!-- Usage -->
<app-template-card
name="Marvel Dashboard"
description="Angular 21 + Bootstrap 5 admin template"
framework="Angular"
[price]="49"
/>
Signals vs RxJS — When to Use Which
| Scenario | Use |
|---|---|
| Component state (counter, toggle, selected item) | signal() |
| Derived state from other state | computed() |
| DOM side effects (theme, scroll position) | effect() |
| HTTP requests | RxJS + HttpClient |
| WebSocket / EventSource streams | RxJS |
| Debounce user input before API call | RxJS debounceTime |
| Multiple async streams combined | RxJS combineLatest |
| Router events | RxJS |
The practical rule: if it's local state that lives in a component, use signals. If it involves time, async, or stream operators, use RxJS. Most Angular 21 apps use both side by side.
toSignal() — Bridging RxJS to Signals
Angular provides toSignal() to convert an observable into a signal, so you can use RxJS for HTTP and then read the result as a signal in templates:
import { Component, inject } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { toSignal } from '@angular/core/rxjs-interop'
@Component({
selector: 'app-templates',
standalone: true,
template: `
@if (templates()) {
@for (t of templates(); track t.id) {
<div>{{ t.name }}</div>
}
} @else {
<p>Loading...</p>
}
`
})
export class TemplatesComponent {
private http = inject(HttpClient)
// Observable converted to signal — no async pipe needed
templates = toSignal(
this.http.get<any[]>('/api/templates')
)
}
No async pipe, no subscription, no ngOnDestroy. toSignal() handles cleanup automatically.
Frequently Asked Questions
Need a Full Bootstrap 5 Admin Dashboard?
Get a complete Angular 21 + Bootstrap 5 dashboard with 50+ components — built by the same team behind BootstrapPlanet.
Browse Templates →Use code FIRST30 for 30% off your first purchase.