Chapter 5: Signals & Reactivity
Signals & Reactivity is the big mental model shift in modern Angular.
This is what makes Angular feel fresh and fast in 2026. Signals are the new heart of reactivity — they replace a ton of the old RxJS + zone.js dance, especially inside components and templates.
We’ll go slow, with real examples you can paste into your project right now (assuming you have Angular 19+). I’ll explain like we’re debugging together on VS Code Live Share.
1. What are Signals? (The simple story)
A signal is just a tiny wrapper around a value that says: “Hey Angular, when someone reads me, remember that. When I change, tell ONLY the people who care.”
- Reading a signal: count() (note the parentheses — it’s a function!)
- Changing it: .set(newValue) or .update(old => old + 1)
There are three main APIs:
- signal() → create a writable signal (mutable state)
- computed() → create a readonly derived signal (automatic calculations)
- effect() → run side effects when signals change (logging, analytics, imperative DOM, etc.)
Huge win: Signals are synchronous, fine-grained, and Zone.js-independent in many cases (especially with zoneless mode coming strong).
2. signal() — The writable one (your source of truth)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import { Component, signal } from '@angular/core'; @Component({...}) export class CounterComponent { count = signal(0); // ← writable signal<number> increment() { this.count.update(v => v + 1); // or .set(5) } reset() { this.count.set(0); } } |
Template:
|
0 1 2 3 4 5 6 7 8 |
<p>Count: {{ count() }}</p> <button (click)="increment()">+1</button> <button (click)="reset()">Reset</button> |
→ Click +1 → screen updates instantly, only the parts reading count() re-evaluate. No full change detection cycle needed if you’re using OnPush + signals.
Writable vs Readonly
- WritableSignal<T> — has .set() and .update(). You own it, you mutate it.
- Signal<T> (readonly) — only .() to read. No mutation methods.
You create readonly versions like this (great for encapsulation):
|
0 1 2 3 4 5 6 7 8 9 |
private _score = signal(100); readonly score = this._score.asReadonly(); // expose only this // Outside can read score() but cannot score.set(999) |
Important: Readonly prevents .set() / .update(), but if the value is an object/array, you can still mutate it deeply (e.g. user().name = ‘new’). Treat objects as immutable for safety — create new ones with spread or structuredClone.
3. computed() — Derived state magic (readonly + lazy + memoized)
Computed signals are read-only and auto-update when their dependencies change.
Key superpowers:
- Lazy: Doesn’t run until first .() read
- Memoized: Caches result until a dependency changes
- Dynamic dependencies: Only tracks signals actually read in the last run
Example — let’s make a smart user profile card:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import { signal, computed } from '@angular/core'; export class UserProfileComponent { name = signal('Rahul from Airoli'); age = signal(28); // Derived isAdult = computed(() => this.age() >= 18); greeting = computed(() => `Hello, ${this.name()}!`); status = computed(() => { if (this.isAdult()) { return `Adult (${this.age()} years)`; } return 'Minor — come back later!'; }); // Conditional dependency example showDetails = signal(false); details = computed(() => { if (!this.showDetails()) { return 'Click to see more'; } // Only read age() if showDetails is true → no unnecessary dependency return `Lives in Airoli, age ${this.age()}`; }); } |
Template:
|
0 1 2 3 4 5 6 7 8 9 10 |
<p>{{ greeting() }}</p> <p>Status: {{ status() }}</p> <p>{{ details() }}</p> <button (click)="showDetails.update(v => !v)">Toggle Details</button> |
→ Change age.set(16) → isAdult(), status() auto-update → details() doesn’t depend on age until showDetails is true → perf win!
When to use computed vs plain method/getter?
- Use computed() when it depends on signals → want reactivity
- Use plain getter when it’s static or non-reactive
|
0 1 2 3 4 5 6 |
get fullName() { return this.first() + ' ' + this.last(); } // no reactivity |
4. effect() — Side effects (last resort, but useful)
Effects run code when signals change — think logging, saving to localStorage, analytics, syncing to non-Angular APIs.
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import { effect } from '@angular/core'; export class LoggerComponent { count = signal(0); constructor() { effect(() => { console.log(`Count changed to: ${this.count()}`); // Side effect: save to localStorage localStorage.setItem('lastCount', this.count().toString()); }); } } |
Rules from teacher:
- Effects always run at least once on creation
- They track signals read inside → re-run only when those change
- Avoid using effects to copy data between signals — use computed() or new linkedSignal() instead
- Prefer computed / linkedSignal / resource first
- Effects are for imperative stuff (DOM manipulation outside Angular, third-party libs)
Untracked helper (read without dependency):
|
0 1 2 3 4 5 6 7 8 9 10 |
effect(() => { console.log(`Current user: ${this.currentUser()}`); // But don't track count for this log console.log(`Silent count: ${untracked(() => this.count())}`); }); |
5. Why Signals Replace Most Observable Use Cases in Templates
In old Angular (pre-17):
- Local state → BehaviorSubject / Subject + .next()
- Template → async pipe: {{ count$ | async }}
- Derived → combineLatest, map, shareReplay
- Subscription hell, memory leaks if forget takeUntil
In 2026 with signals:
- No subscription needed
- No async pipe in 90% cases
- Synchronous reads in templates → {{ count() }}
- Fine-grained: Angular tracks exactly which signals are read in a template/computed → updates only affected DOM bits
- Better perf with OnPush + zoneless future
- Simpler mental model: no cold/hot observables, no operators for simple cases
When to still use RxJS/Observables (2026 reality)
- Async streams: HTTP (but new resource() + toSignal() bridges it)
- WebSockets, timers, mouse move streams, debounce/throttle
- Complex operators (switchMap, exhaustMap, etc.)
- Interop: toObservable(signal) and toSignal(observable)
Signals handle synchronous + derived UI state → RxJS for streams/events/async
Quick Cheat Sheet Table
| API | Mutable? | Use Case | Example Read | Triggers Update? |
|---|---|---|---|---|
| signal() | Yes | Source of truth, user input, counters | count() | Yes (set/update) |
| computed() | No | Derived values, filters, formatting | double() | When deps change |
| effect() | N/A | Logging, sync to localStorage, 3rd-party | N/A (side effect) | When deps change |
| input() | No | Parent → child (readonly signal) | name() | When parent changes input |
Mini Practice Task (do this!)
In your app.component.ts or a new component:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
name = signal('Webliance'); city = signal('Airoli, Maharashtra'); mood = signal<'happy' | 'learning' | 'tired'>('learning'); profile = computed(() => ({ greeting: `Hi ${this.name()} from ${this.city()}!`, status: this.mood() === 'learning' ? 'Crushing Angular 🚀' : this.mood() })); toggleMood() { this.mood.set(this.mood() === 'learning' ? 'happy' : 'learning'); } |
Template:
|
0 1 2 3 4 5 6 7 8 |
<p>{{ profile().greeting }}</p> <p>Status: {{ profile().status }}</p> <button (click)="toggleMood()">Change Mood</button> |
→ Change mood → only profile recomputes → smooth!
You’ve now got the core reactivity model of modern Angular.
Next is usually Services + Dependency Injection (where we share signals across components). Ready, or want more signal examples (forms integration, async with toSignal, linkedSignal intro, common pitfalls like reading signals outside reactive context)? Tell me! 😊
