๐Ÿณ IntuitiveFE
Login
โ† All concepts

Angular Signals

โฑ๏ธ ~5-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

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

Featuresignal()BehaviorSubject
Read syntaxcount()count$.value or count$ | async
Write syntaxcount.set(1)count$.next(1)
Derive valuecomputed(() => count() * 2)count$.pipe(map(n => n * 2))
Subscribeautomatic (template / effect)manual subscribe / async pipe
Cleanupautomatic (injection context)manual unsubscribe / takeUntilDestroyed
Async opsuse RxJS + toSignal()native with operators
Change detectionfine-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.

signal() โ€” Angular 17+ code example
js
1import { 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})
13export 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" โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
43const 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});
Gotcha: .mutate() was removed in Angular 17. Use .update() with a spread or structuredClone instead. Signals use === equality by default โ€” mutating an object in place won't trigger dependents. Always produce a new reference.
Insight: Reading a signal inside a reactive context (template, computed, effect) automatically registers a dependency. Angular's runtime builds a reactive graph at runtime โ€” no explicit dependency declaration needed, unlike RxJS which requires explicit subscription wiring.
Explored 1/5:๐Ÿ“ก๐Ÿงฎโšก๐ŸŒ‰๐Ÿ“ฅ
๐ŸŽฏ

Challenge

Explore all 5 signal patterns. In the graph below, change the base signals and watch the computed signals update.

Try it
๐ŸŽฏ

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

js
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ย 
6const a = signal(1);
7const b = signal(2);
8const sum = computed(() => a() + b()); // UNSET
9ย 
10sum(); // โ†’ 3, now CLEAN
11sum(); // โ†’ 3, still CLEAN (no deps changed), no recomputation
12ย 
13a.set(10); // sum becomes DIRTY
14sum(); // โ†’ 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

js
1import { 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?
5const options = signal(['A', 'B', 'C']);
6ย 
7// linkedSignal: derived from options, but also writable
8const selectedOption = linkedSignal({
9 source: options,
10 computation: (opts) => opts[0], // default: first option
11});
12ย 
13selectedOption(); // โ†’ 'A' (derived from options)
14selectedOption.set('B'); // โ†’ 'B' (manually overridden)
15options.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.

js
1import { afterNextRender, ElementRef, inject } from '@angular/core';
2ย 
3@Component({ standalone: true, template: `<canvas #chart></canvas>` })
4export 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.
1/4

How does Angular detect when to re-render a template that reads signals?