Angular Signals
โฑ๏ธ ~5-minute bite ยท solve the sandbox to master
5-Year-Old Metaphor
โ The physical, real-world picture. No jargon.๐ Signals are live spreadsheet cells. Cell B1 = A1 * 2. When you change A1, B1 updates automatically โ no manual recalculation, no explicit subscriptions. Angular signals work exactly like this: change a signal, all computed() that depend on it re-evaluate lazily.
๐ก
signal()
A writable cell. Read with (). Write with .set() or .update(). Other signals can depend on it.
๐งฎ
computed()
A formula cell. Lazy + memoized. Re-evaluates only when a dependency changes and it's read.
โก
effect()
A macro that runs whenever its deps change. For side effects: logging, storage sync, DOM manipulation.
Signals vs BehaviorSubject โ at a glance
| Feature | signal() | BehaviorSubject |
|---|---|---|
| Read syntax | count() | count$.value or count$ | async |
| Write syntax | count.set(1) | count$.next(1) |
| Derive value | computed(() => count() * 2) | count$.pipe(map(n => n * 2)) |
| Subscribe | automatic (template / effect) | manual subscribe / async pipe |
| Cleanup | automatic (injection context) | manual unsubscribe / takeUntilDestroyed |
| Async ops | use RxJS + toSignal() | native with operators |
| Change detection | fine-grained (signal scheduler) | markForCheck() or async pipe |
Interactive Sandbox
โ Move something, see it react instantly.Pattern
Signal dependency graph โ change base signals, watch computed cascade
Base signals (writable)
\$10
3 items
0% off
Computed signals (read-only)
subtotal = computed()
price() ร quantity()
$30.00
total = computed()
subtotal() ร (1 โ discount() / 100)
$30.00
Dependency graph:
price, qty โ subtotal
subtotal, discount โ total
Move sliders to see reactive updates
When to use
Local component state that should drive template re-rendering. Replace simple useState-like patterns. Any value that other computed() signals or effects() need to react to.
| 1 | import { signal, Component } from '@angular/core'; |
| 2 | ย |
| 3 | @Component({ |
| 4 | standalone: true, |
| 5 | template: ` |
| 6 | <p>Count: {{ count() }}</p> |
| 7 | <p>Name: {{ name() }}</p> |
| 8 | <button (click)="increment()">+1</button> |
| 9 | <button (click)="reset()">Reset</button> |
| 10 | <input [value]="name()" (input)="name.set($any($event.target).value)" /> |
| 11 | `, |
| 12 | }) |
| 13 | export class CounterComponent { |
| 14 | // Create signals โ generic type inferred |
| 15 | count = signal(0); // WritableSignal<number> |
| 16 | name = signal('Angular'); // WritableSignal<string> |
| 17 | user = signal<User | null>(null); // WritableSignal<User | null> |
| 18 | ย |
| 19 | increment() { |
| 20 | // .set() โ replace the value entirely |
| 21 | this.count.set(this.count() + 1); |
| 22 | ย |
| 23 | // .update() โ functional: receive current value, return next value |
| 24 | // Preferred when new value depends on the old one |
| 25 | this.count.update((n) => n + 1); |
| 26 | } |
| 27 | ย |
| 28 | reset() { |
| 29 | this.count.set(0); |
| 30 | } |
| 31 | ย |
| 32 | updateUser(patch: Partial<User>) { |
| 33 | // .update() with spread โ immutable update for objects |
| 34 | this.user.update((u) => u ? { ...u, ...patch } : null); |
| 35 | } |
| 36 | } |
| 37 | ย |
| 38 | // โโ Reading a signal โ the () call registers the dependency โโโ |
| 39 | // Any computed() or effect() that calls count() will re-run |
| 40 | // automatically when count changes. This is the reactive graph. |
| 41 | ย |
| 42 | // โโ Equality function โ control when a signal "changes" โโโโโโโโ |
| 43 | const filtered = signal(['a', 'b'], { |
| 44 | // Custom equality: same contents = no change notification |
| 45 | equal: (a, b) => a.length === b.length && a.every((v, i) => v === b[i]), |
| 46 | }); |
Challenge
Explore all 5 signal patterns. In the graph below, change the base signals and watch the computed signals update.
Why Should I Care?
โ The exact interview question + the bug it kills.Interview questions
Q: How are signals different from BehaviorSubject?
Signals are synchronous and readable without subscription โ just call the signal like a function. BehaviorSubject requires subscribe() or the async pipe to get values. Signals automatically track dependencies: if you read two signals inside computed(), both are tracked as dependencies with no explicit declaration. BehaviorSubject dependencies must be wired manually with combineLatest or pipe operators. Signals also handle their own cleanup โ no takeUntil/unsubscribe needed in component context.
Q: Do signals replace RxJS entirely?
No โ RxJS and signals solve different problems. RxJS is excellent for complex async pipelines: retry logic, debouncing, merging multiple HTTP streams, WebSocket handling. Signals are better for local/derived state: UI state, form state, computed display values. The recommended pattern is to keep RxJS in services for async work and use toSignal() at the component boundary to convert to signals for reactive template binding.
Q: How do signals improve change detection?
With zone.js, any async event triggers a full CD cycle โ every component is checked. With signals, Angular knows exactly which template expressions read which signals. When a signal changes, only those specific expressions are re-evaluated โ no full-tree check. This is fine-grained reactivity: O(affected signals) instead of O(component tree size). The eventual goal is zoneless Angular where signals drive all CD and zone.js is removed entirely.
The Deep Dive
โ Spec refs, engine internals, the minutiae.Signal internals โ producer/consumer model
Every signal is a producer. Every computed() and effect() is a consumer. When a consumer reads a signal (calls it with ()), the signal registers the consumer in its subscriber list. When the signal value changes, it marks all registered consumers as dirty. Computed consumers are lazy โ they don't recompute until they are read again. Effect consumers are eager โ they schedule a re-run in the next microtask. Angular's template is a consumer: it reads signals during rendering and subscribes to them automatically.
computed() memoization and dirty tracking
| 1 | // Computed has three states: |
| 2 | // 1. UNSET โ never computed (first read triggers computation) |
| 3 | // 2. CLEAN โ computed, no dependencies changed (return cached) |
| 4 | // 3. DIRTY โ a dependency changed (recompute on next read) |
| 5 | ย |
| 6 | const a = signal(1); |
| 7 | const b = signal(2); |
| 8 | const sum = computed(() => a() + b()); // UNSET |
| 9 | ย |
| 10 | sum(); // โ 3, now CLEAN |
| 11 | sum(); // โ 3, still CLEAN (no deps changed), no recomputation |
| 12 | ย |
| 13 | a.set(10); // sum becomes DIRTY |
| 14 | sum(); // โ 12, recomputed, now CLEAN again |
| 15 | ย |
| 16 | // If nobody reads sum(), it stays DIRTY โ zero wasted computation |
| 17 | // This is the "lazy" property of computed() |
linkedSignal() โ Angular 18+ derived writable signal
| 1 | import { linkedSignal, signal } from '@angular/core'; |
| 2 | ย |
| 3 | // Problem: computed() is read-only. What if you need a derived value |
| 4 | // that can also be manually overridden? |
| 5 | const options = signal(['A', 'B', 'C']); |
| 6 | ย |
| 7 | // linkedSignal: derived from options, but also writable |
| 8 | const selectedOption = linkedSignal({ |
| 9 | source: options, |
| 10 | computation: (opts) => opts[0], // default: first option |
| 11 | }); |
| 12 | ย |
| 13 | selectedOption(); // โ 'A' (derived from options) |
| 14 | selectedOption.set('B'); // โ 'B' (manually overridden) |
| 15 | options.set(['X', 'Y', 'Z']); // โ 'X' (source changed โ resets to computation) |
| 16 | ย |
| 17 | // Use case: pagination โ default to page 1 when filter changes, |
| 18 | // but allow manual page navigation otherwise |
afterNextRender() โ DOM access in signal world
In a signals-based component, there is no reliable AfterViewInit equivalent for DOM access โ signals run outside the Angular zone. afterNextRender() (Angular 17+) schedules a callback to run after the next render pass completes, on the client only (not during SSR). Use it instead of ngAfterViewInit for signal-based components that need to measure DOM or initialize third-party libraries.
| 1 | import { afterNextRender, ElementRef, inject } from '@angular/core'; |
| 2 | ย |
| 3 | @Component({ standalone: true, template: `<canvas #chart></canvas>` }) |
| 4 | export class ChartComponent { |
| 5 | private el = inject(ElementRef); |
| 6 | ย |
| 7 | constructor() { |
| 8 | afterNextRender(() => { |
| 9 | // Safe: DOM is ready, runs client-side only |
| 10 | const canvas = this.el.nativeElement.querySelector('canvas'); |
| 11 | initChart(canvas); |
| 12 | }); |
| 13 | } |
| 14 | } |
Interview Questions
โ Real questions from real interviews โ with answers.Signals are synchronous and auto-track dependencies; BehaviorSubject requires manual subscribe/pipe wiring.
.set() replaces the value; .update() receives the current value and returns the next value.
Lazy: doesn't compute until read. Memoized: caches the result until a dependency changes.
Writing inside effect() can cause infinite loops. Use { allowSignalWrites: true } sparingly; prefer computed() or linkedSignal().
toSignal(obs$) subscribes immediately and returns a Signal that holds the latest emitted value.
linkedSignal() is a writable derived signal that resets to a computation when its source changes.
Memory Game
โ Quick quiz โ lock the concept in long-term memory.How does Angular detect when to re-render a template that reads signals?