Angular 47 🅰️ Signals — Reactive Primitives
Signals are Angular’s reactive primitive. A signal is a value that notifies its consumers when it changes, and a component that reads a signal is marked for change detection when the signal changes. The API is small — signal, computed, and effect are the three functions — but the model they implement is the foundation of the modern Angular change detection. Unlike a BehaviorSubject, a signal is read synchronously with a function call, it tracks its dependencies automatically, and it does not need a subscription. The result is simpler code: no async pipe in the template, no subscribe in the constructor, no unsubscribe in ngOnDestroy. This chapter covers the three primitives, the graph they form, the change detection model, the difference between a signal and a BehaviorSubject, and the patterns that make signal-based code predictable. It is the foundation for the interop with RxJS covered in Angular 46 and the state patterns that follow.
Key point: A signal(initial) creates a writable signal with a current value. A computed(fn) creates a read-only signal whose value is derived from other signals; it recomputes only when its dependencies change. An effect(fn) runs a side effect when any of the signals it reads change, and it re-runs whenever they change. Signals are read with (), written with .set() or .update(), and they are synchronous. A component that reads a signal in its template is automatically marked for check when the signal changes. The dependency graph is tracked at runtime, and it updates automatically as the reads change.
The three primitives
The signal API is three functions. Everything else is composition.
signal(initial). Creates a writable signal.
import { signal } from '@angular/core';
const count = signal(0);
console.log(count()); // 0
count.set(1);
console.log(count()); // 1
count.update((v) => v + 1);
console.log(count()); // 2
The signal holds a value, and the value is read by calling the signal as a function. The set method replaces the value, and the update method computes a new value from the current one.
Why the read is a function call. The function call is what allows the signal to track the read. When the signal is read inside a computed or an effect, the read is recorded, and the dependency is established. A property access would not have the same tracking. The () is the mechanism.
computed(fn). Creates a read-only signal whose value is derived.
const count = signal(0);
const doubled = computed(() => count() * 2);
console.log(doubled()); // 0
count.set(5);
console.log(doubled()); // 10
The computed tracks the signals it reads, and it recomputes when any of them changes. The result is cached — the computation runs only when the dependencies change and the value is read.
Why the computed is lazy. The computation does not run until the value is read. The computed is a recipe, and the recipe runs when the value is needed. The laziness is what makes the graph efficient: a computed that is not read does not compute.
effect(fn). Runs a side effect when the signals it reads change.
effect(() => {
console.log('count is', count());
});
// count is 0
count.set(1);
// count is 1
The effect reads the signal, and the read is recorded. When the signal changes, the effect re-runs. The effect is the escape hatch to the imperative world — the DOM update, the log, the external call.
Why the effect runs at least once. The effect runs immediately on creation, then on each dependency change. The first run is the initial setup, and the subsequent runs are the reactions. The immediate run is what makes the effect useful for the initial side effect.
Why the three are the complete API. The signal is the state, the computed is the derivation, and the effect is the side effect. Everything else — the linkedSignal, the resource, the toSignal — is built on the three. The primitives are the vocabulary, and the vocabulary is small.
Reading and writing
A signal is read by calling it. A writable signal is written with set or update.
The read. count() returns the current value.
const count = signal(0);
const value = count(); // 0
The read is the only way to get the value. There is no .value property; the function call is the API.
The write. set replaces the value, and update computes a new one from the current.
count.set(5);
count.update((v) => v + 1);
The set is for a known value, and the update is for a value that depends on the current one. Both trigger the notification to the consumers.
Why the signal is not a property. The function-call API is deliberate. It makes the read explicit, which is what the tracking needs. It also prevents the accidental assignment that a property would allow — count = 5 would be a mistake, and the compiler would reject it.
Why the update is the preferred form for dependent values. The count.update((v) => v + 1) is atomic. The value is read, the computation runs, and the value is set in one operation. The count.set(count() + 1) reads and writes in two steps, which is correct in a synchronous context but is not atomic. The update is the safer form.
Why the signal can hold any type. The signal is generic, and the value can be a primitive, an object, an array, or a function. The signal does not care; it holds the value and notifies the consumers.
Why the object signal should be updated immutably. A signal that holds an object notifies its consumers when the reference changes. A mutation of the object does not change the reference, so the consumers are not notified. The immutable update — obj.set({ ...obj(), field: value }) — is the pattern that makes the notification work.
const user = signal({ name: 'Alice', age: 30 });
user.update((u) => ({ ...u, age: 31 })); // new reference, notification
Why the immutability is important for the computed. A computed that reads a signal’s object property depends on the signal’s reference, not the property. The mutation does not change the reference, so the computed does not recompute. The immutable update is what makes the dependency tracking correct.
Why the set and update are the only writers. The signal’s value is changed only through the two methods. The internal state is not accessible otherwise. The encapsulation is what makes the tracking reliable — every change goes through the notification.
Computed signals
A computed signal is a derived value. It is read-only, and its value is computed from other signals.
const firstName = signal('Alice');
const lastName = signal('Smith');
const fullName = computed(() => `${firstName()} ${lastName()}`);
console.log(fullName()); // Alice Smith
firstName.set('Bob');
console.log(fullName()); // Bob Smith
The fullName reads the two signals, and the reads are recorded. When either changes, the fullName recomputes.
Why the computed is lazy and memoized. The computation runs when the value is read, and the result is cached. If the dependencies have not changed since the last read, the cached value is returned without recomputation. The laziness and the memoization are what make the computed efficient.
Why the computed is read-only. A computed has no set or update. The value is derived, and the only way to change it is to change its dependencies. The read-only nature is what makes the dependency graph a tree — the values flow in one direction, from the sources to the derived.
Why the computed can have side effects in the computation. The computation function should be pure. A side effect in the computation would run whenever the computed recomputes, which is unpredictable. The side effects belong in an effect, not a computed.
Why the computed can depend on other computeds. A computed can read other computeds, and the graph is built. The dependencies are transitive: a computed that reads a computed that reads a signal depends on the signal. The graph is what makes the model compositional.
const price = signal(100);
const quantity = signal(2);
const subtotal = computed(() => price() * quantity());
const tax = computed(() => subtotal() * 0.1);
const total = computed(() => subtotal() + tax());
console.log(total()); // 220
price.set(200);
console.log(total()); // 440
The total depends on subtotal and tax, which depend on price and quantity. The change to price propagates through the graph, and the total recomputes.
Why the graph is efficient. A computed that is not read does not compute. A computed whose dependencies have not changed returns the cached value. The graph recomputes only the nodes that are read and whose dependencies have changed. The efficiency is the point of the model.
Why the computed can be expensive. A computed that reads many signals and does a lot of work is expensive when it recomputes. The memoization helps, but the first read after a change is the full computation. The expensive computations should be split into smaller computeds, or moved to a different mechanism.
Why the computed should not be used for a side effect. The computed is for the derived value, and the side effect belongs in an effect. A computed that logs or fetches is a mistake — the computation should be pure, and the side effect should be separate.
Effects
An effect runs a side effect when the signals it reads change. It is the mechanism for the imperative work that the signal model cannot express.
effect(() => {
const count = countSignal();
console.log('count changed to', count);
});
The effect reads the signal, and the read is recorded. The effect re-runs when the signal changes.
Why the effect exists. A signal is a value, and a computed is a derivation. The side effects — updating the DOM directly, persisting to localStorage, logging, calling an external API — are not values, and the effect is where they run. The effect is the bridge from the signal world to the imperative world.
Why the effect runs after change detection. The effect runs after the signal’s change is committed, which is after the change detection that the signal triggered. The timing matters for the DOM reads and writes, which should happen after the state is settled.
Why the effect should be used sparingly. The Angular team’s guidance is to use computed for the derived values and effect only for the side effects. The effect is the escape hatch, and it should not be the place where the state is computed. The state belongs in the signals, the derived values in the computeds, and the side effects in the effects.
Why the effect tracks the signals it reads. The effect’s function reads the signals, and the reads are recorded. The effect re-runs when any of them changes. The tracking is the same as the computed’s, and the effect’s dependencies are the signals it reads.
Why the effect can be cleaned up. The effect can return a cleanup function, which runs when the effect re-runs or when the effect is destroyed.
effect((onCleanup) => {
const timer = setInterval(() => count(), 1000);
onCleanup(() => clearInterval(timer));
});
The cleanup function releases the resource. The pattern is the same as the Observable’s teardown, and it is the mechanism for the effects that create resources.
Why the effect’s cleanup runs on destroy. The cleanup runs when the effect is destroyed, which is when the component or the service is destroyed. The resources are released, and the leaks are prevented. The cleanup is the mechanism that makes the effect safe.
Why the effect should not write to a signal it reads. An effect that writes to a signal it reads creates a loop. The signal change triggers the effect, which writes to the signal, which triggers the effect again. The Angular team allows the write but warns about the loop. The safe pattern is to write to a different signal, or to use a computed for the derived value.
Why the effect is the modern replacement for the BehaviorSubject subscription. The old pattern was to inject a service, subscribe to its BehaviorSubject in the constructor, and update the component’s state in the subscription. The modern pattern is to read the signal in a computed or an effect. The subscription is gone, and the signal is read. The code is simpler, and the cleanup is automatic.
Change detection
Signals integrate with Angular’s change detection. A component that reads a signal in its template is marked for check when the signal changes.
@Component({
selector: 'app-counter',
standalone: true,
template: `
<p>Count: {{ count() }}</p>
<p>Doubled: {{ doubled() }}</p>
<button (click)="increment()">+</button>
`,
})
export class CounterComponent {
readonly count = signal(0);
readonly doubled = computed(() => this.count() * 2);
increment(): void {
this.count.update((v) => v + 1);
}
}
The template reads count() and doubled(). The reads are recorded, and the component is marked for check when either changes. The change detection runs, and the template is updated.
Why the read in the template is the marker. The template’s read of the signal is the subscription. The component is registered as a consumer of the signal, and the signal’s change marks the component for check. The registration is automatic, and there is no manual subscription.
Why the change detection is more efficient. The traditional change detection checks every component in the tree. The signal-based change detection checks only the components that read the changed signals. The efficiency is the point of the integration.
Why the signal read must be in the template. A signal read in the component class does not mark the component for check. The read must be in the template, or in a computed that the template reads, or in an effect. The template read is what ties the signal to the change detection.
Why the OnPush strategy is compatible. A signal-based component can use OnPush, and the signal’s change marks it for check. The OnPush strategy and the signals are a natural pair — the component is checked when its inputs change or its signals change.
Why the change detection is glitch-free. The signal graph ensures that the computed values are consistent when the change detection runs. A computed that depends on two signals sees both at their new values, not one at the old and one at the new. The consistency is a property of the graph, and it is what makes the derived values reliable.
Why the signal read is a dependency, not a value. The template reads the signal’s value, and the read is recorded as a dependency. The value is what is rendered, and the dependency is what triggers the re-render. The two are tied, and the model is automatic.
Why the signal model is simpler than the zone.js model. The zone.js model patches the asynchronous APIs and triggers the change detection after each event. The signal model tracks the reads and marks the components directly. The signal model is more targeted, and it does not require the patching.
Signals vs BehaviorSubject
A signal and a BehaviorSubject both hold a current value and notify their consumers. The difference is in the API and the model.
| Aspect | Signal | BehaviorSubject |
|---|---|---|
| Read | sig() synchronous | subj.value or subscribe |
| Write | sig.set(v) | subj.next(v) |
| Derived | computed | combineLatest, map |
| Side effect | effect | subscribe |
| Cleanup | Automatic | Manual or takeUntil |
| Template | Direct sig() | async pipe |
| Change detection | Automatic | Manual or async pipe |
| Dependency tracking | Automatic | Manual |
Why the signal is synchronous. The signal’s value is available at any time with (). The BehaviorSubject‘s value is available with .value, but the notification is asynchronous, and the consumers must subscribe. The synchronous read is simpler.
Why the signal tracks automatically. The signal’s dependencies are the reads, and the reads are recorded. The BehaviorSubject‘s dependencies are the subscriptions, which the developer writes. The automatic tracking is the main advantage.
Why the signal’s cleanup is automatic. The effect and the computed are cleaned up when their context is destroyed. The BehaviorSubject‘s subscription must be cleaned up with takeUntil, takeUntilDestroyed, or a manual unsubscribe. The automatic cleanup removes a class of leaks.
Why the BehaviorSubject is still used. The BehaviorSubject is part of the RxJS ecosystem, and it composes with the operators. A stream that is combined with other streams and transformed with operators is an RxJS job, and the BehaviorSubject is the right tool. The signal is for the state, and the BehaviorSubject is for the stream.
Why the choice is per-value. A value can start as a signal (the state), be converted to an Observable (for the pipeline), and converted back to a signal (for the template). The choice is about which model fits the value at each stage, and the conversions are cheap.
Why the signals are the modern recommendation. The Angular team recommends signals for the state and the RxJS for the streams. The two are complementary, and the signals are the newer primitive with the better change detection integration. The BehaviorSubject remains for the cases where the RxJS operators are needed.
Why the migration is gradual. A project can adopt signals incrementally. The toSignal converts an Observable to a signal, and the toObservable converts a signal to an Observable. The two can coexist, and the migration does not require a rewrite.
Common pitfalls
The signals are simple, but the patterns have pitfalls.
Mutating an object in a signal. A signal that holds an object notifies its consumers when the reference changes. A mutation does not change the reference, and the consumers are not notified. The immutable update is the fix.
// Wrong
user().name = 'Bob';
// Right
user.update((u) => ({ ...u, name: 'Bob' }));
Reading a signal in a computed without the call. The signal must be called with (). A computed that reads count instead of count() reads the function, not the value. The TypeScript compiler rejects the wrong type in most cases, but the mistake is subtle.
Writing to a signal in a computed. A computed is for the derived value. A write to a signal in the computation is a side effect, and it makes the computation impure. The write belongs in an effect or in a method.
The effect that writes to a signal it reads. The loop is the mistake. The effect that reads count and writes count re-triggers itself. The fix is to write to a different signal or to use a computed.
The effect that fetches data. An effect is for the side effects, not the data fetching. The fetch should be in a switchMap of the signal’s stream, or in the resource API. The effect that fetches is a common mistake, and it produces a request on every dependency change.
The computed that is not read. A computed that is never read does not compute, and its dependencies are not tracked. The computed is lazy, and the laziness is a feature, not a bug. But a computed that is expected to run and does not is a surprise.
The signal in an OnPush component without the template read. The signal must be read in the template to mark the component for check. A signal read only in the component class does not trigger the change detection. The read in the template is the marker.
Why the pitfalls are about the model. Each pitfall is a misunderstanding of the model — the immutability, the laziness, the purity, the tracking. The signals are simple, but the model has rules, and the rules are what make the model work.
Why the signal model is worth the learning. The signals replace a class of manual subscription management with automatic tracking and cleanup. The code is simpler, the change detection is more efficient, and the derived values are consistent. The learning curve is small — three functions — and the benefit is the simpler code. The model is the modern Angular, and the RxJS remains for the streams.
Complete Example Session
import { Component, Injectable, signal, computed, effect, inject } from '@angular/core';
// ============================================
// PART 1: A WRITABLE SIGNAL
// ============================================
const count = signal(0);
console.log(count()); // 0
count.set(1);
count.update((v) => v + 1);
console.log(count()); // 2
// ============================================
// PART 2: A COMPUTED
// ============================================
const price = signal(100);
const quantity = signal(2);
const subtotal = computed(() => price() * quantity());
const tax = computed(() => subtotal() * 0.1);
const total = computed(() => subtotal() + tax());
console.log(total()); // 220
price.set(200);
console.log(total()); // 440
// ============================================
// PART 3: AN EFFECT
// ============================================
effect(() => {
console.log('total is', total());
});
// total is 220
price.set(300);
// total is 660
// ============================================
// PART 4: THE IMMUTABLE UPDATE
// ============================================
interface User {
name: string;
age: number;
}
const user = signal<User>({ name: 'Alice', age: 30 });
// Wrong:
// user().age = 31; // mutation, no notification
// Right:
user.update((u) => ({ ...u, age: 31 }));
// ============================================
// PART 5: THE COMPONENT
// ============================================
@Component({
selector: 'app-counter',
standalone: true,
template: `
<p>Count: {{ count() }}</p>
<p>Doubled: {{ doubled() }}</p>
<button (click)="increment()">+</button>
<button (click)="reset()">Reset</button>
`,
})
export class CounterComponent {
readonly count = signal(0);
readonly doubled = computed(() => this.count() * 2);
increment(): void {
this.count.update((v) => v + 1);
}
reset(): void {
this.count.set(0);
}
}
// ============================================
// PART 6: THE SERVICE
// ============================================
@Injectable({ providedIn: 'root' })
export class CartService {
private readonly items = signal<CartItem[]>([]);
readonly cartItems = this.items.asReadonly();
readonly total = computed(() =>
this.items().reduce((sum, item) => sum + item.price * item.quantity, 0),
);
readonly count = computed(() =>
this.items().reduce((sum, item) => sum + item.quantity, 0),
);
add(item: CartItem): void {
this.items.update((list) => [...list, item]);
}
remove(id: string): void {
this.items.update((list) => list.filter((i) => i.id !== id));
}
clear(): void {
this.items.set([]);
}
}
// ============================================
// PART 7: THE EFFECT WITH CLEANUP
// ============================================
@Component({ selector: 'app-clock', standalone: true, template: `{{ time() }}` })
export class ClockComponent {
readonly time = signal(new Date().toLocaleTimeString());
constructor() {
effect((onCleanup) => {
const timer = setInterval(() => {
this.time.set(new Date().toLocaleTimeString());
}, 1000);
onCleanup(() => clearInterval(timer));
});
}
}
// ============================================
// PART 8: THE PERSISTENCE EFFECT
// ============================================
@Component({ selector: 'app-settings', standalone: true, template: `` })
export class SettingsComponent {
private readonly settings = inject(SettingsService);
constructor() {
effect(() => {
localStorage.setItem('settings', JSON.stringify(this.settings.current()));
});
}
}
// ============================================
// PART 9: THE READ-ONLY SIGNAL
// ============================================
@Injectable({ providedIn: 'root' })
export class UserService {
private readonly user = signal<User | null>(null);
readonly currentUser = this.user.asReadonly();
setUser(user: User): void {
this.user.set(user);
}
clear(): void {
this.user.set(null);
}
}
// The consumer reads currentUser() but cannot set it.
// ============================================
// PART 10: WHAT NOT TO DO
// ============================================
// Don't mutate an object in a signal
// user().name = 'Bob'; // no notification
// Don't read the signal without the call
// const value = count; // the function, not the value
// Don't write to a signal in a computed
// computed(() => { count.set(1); return count(); }); // impure
// Don't use an effect for data fetching
// effect(() => { http.get('/api').subscribe(); }); // wrong tool
// Don't write to a signal the effect reads
// effect(() => { count.set(count() + 1); }); // loop
// Don't expose the writable signal
// readonly items = this.items; // use asReadonly()
// Don't expect a signal read in the class to trigger change detection
// The read must be in the template.
The ten parts cover the primitives, the immutable update, the component, the service, the effect with cleanup, the persistence effect, the read-only signal, and the anti-patterns.
Quick Reference
The Primitives
| Function | Purpose |
|---|---|
signal(initial) | Writable signal |
computed(fn) | Read-only derived signal |
effect(fn) | Side effect on change |
The Signal API
| Method | Purpose |
|---|---|
sig() | Read the value |
sig.set(v) | Replace the value |
sig.update(fn) | Compute a new value |
sig.asReadonly() | Read-only view |
The Computed API
| Method | Purpose |
|---|---|
comp() | Read the derived value |
| — | No set, no update |
The Effect API
| Feature | Purpose |
|---|---|
effect(fn) | Run on dependency change |
onCleanup(fn) | Release resources |
Signals vs BehaviorSubject
| Aspect | Signal | BehaviorSubject |
|---|---|---|
| Read | sig() | .value or subscribe |
| Write | .set() | .next() |
| Derived | computed | Operators |
| Side effect | effect | Subscribe |
| Cleanup | Automatic | Manual |
| Template | Direct | async pipe |
The Patterns
| Pattern | Code |
|---|---|
| State | signal(initial) |
| Derived | computed(() => ...) |
| Side effect | effect(() => ...) |
| Read-only export | sig.asReadonly() |
| Immutable update | sig.update((v) => ({ ...v, x })) |
Best Practices
✅ Do This:
// Use signal for the state
readonly count = signal(0); // ✅
// Use computed for the derived value
readonly doubled = computed(() => this.count() * 2); // ✅
// Use effect for the side effect
effect(() => document.body.classList.toggle('dark', this.theme() === 'dark')); // ✅
// Use update for the dependent value
this.count.update((v) => v + 1); // ✅
// Use the immutable update for an object
this.user.update((u) => ({ ...u, age: 31 })); // ✅
// Use asReadonly for the public API
readonly items = this.items.asReadonly(); // ✅
// Use onCleanup for the resources
effect((onCleanup) => { const t = setInterval(...); onCleanup(() => clearInterval(t)); }); // ✅
// Read the signal in the template for the change detection
template: `{{ count() }}` // ✅
❌ Don’t Do This:
// Don't mutate an object in a signal
this.user().age = 31; // no notification // ⚠️
// Don't read the signal without the call
const value = this.count; // the function // ⚠️
// Don't write to a signal in a computed
computed(() => { this.count.set(1); return this.count(); }); // ⚠️
// Don't use an effect for data fetching
effect(() => { this.http.get('/api').subscribe(); }); // ⚠️
// Don't write to a signal the effect reads
effect(() => { this.count.set(this.count() + 1); }); // ⚠️
// Don't expose the writable signal
readonly items = this.items; // use asReadonly() // ⚠️
// Don't expect a class-only read to trigger change detection
// The read must be in the template. // ⚠️
// Don't put a side effect in a computed
computed(() => { console.log('computing'); return this.count() * 2; }); // ⚠️
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| Object mutation | No notification | Immutable update |
Missing () | Reads the function | Call the signal |
| Write in a computed | Impure | Use effect |
| Effect fetches data | Wrong tool | Use switchMap |
| Effect writes to its source | Loop | Use a computed |
| Writable signal exposed | External writes | asReadonly() |
| Class-only read | No change detection | Read in the template |
| Side effect in a computed | Impure | Move to effect |
Real-World Examples
1. Counter
readonly count = signal(0);
2. Derived value
readonly doubled = computed(() => this.count() * 2);
3. Cart total
readonly total = computed(() => this.items().reduce((s, i) => s + i.price, 0));
4. Read-only export
readonly items = this.items.asReadonly();
5. Immutable add
this.items.update((list) => [...list, item]);
6. Immutable remove
this.items.update((list) => list.filter((i) => i.id !== id));
7. Effect for the DOM
effect(() => document.body.classList.toggle('dark', this.theme() === 'dark'));
8. Effect for persistence
effect(() => localStorage.setItem('settings', JSON.stringify(this.settings())));
9. Effect with cleanup
effect((onCleanup) => { const t = setInterval(...); onCleanup(() => clearInterval(t)); });
10. Component template
template: `{{ count() }} — {{ doubled() }}`
Visual: The Three Primitives
┌──────────────────────────────────────────────────────────┐
│ signal(initial) │
│ Writable. Read with (). Set with .set(). │
│ The source of truth. │
│ │
├──────────────────────────────────────────────────────────┤
│ computed(fn) │
│ Read-only. Derived. Lazy and memoized. │
│ Recomputes when the dependencies change. │
│ │
├──────────────────────────────────────────────────────────┤
│ effect(fn) │
│ Side effect. Runs on dependency change. │
│ The escape hatch to the imperative world. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: The Dependency Graph
┌──────────────────────────────────────────────────────────┐
│ price: signal(100) │
│ quantity: signal(2) │
│ │ │
│ ▼ │
│ subtotal = computed(() => price() * quantity()) │
│ │ │
│ ├──► tax = computed(() => subtotal() * 0.1) │
│ │ │
│ └──► total = computed(() => subtotal() + tax()) │
│ │
│ price.set(200) │
│ │ │
│ ▼ │
│ subtotal recomputes → tax recomputes → total recomputes │
│ │
│ Only the nodes that are read recompute. │
│ The memoization caches the values. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: Change Detection
┌──────────────────────────────────────────────────────────┐
│ TEMPLATE │
│ {{ count() }} │
│ │ │
│ └── the read registers the component as a │
│ consumer of the signal │
│ │
│ count.set(1) │
│ │ │
│ ▼ │
│ The signal notifies its consumers. │
│ │ │
│ ▼ │
│ The component is marked for check. │
│ │ │
│ ▼ │
│ Change detection runs. │
│ │ │
│ ▼ │
│ The template is updated. │
│ │
│ Only the components that read the signal are checked. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: Signals vs BehaviorSubject
┌──────────────────────────────────────────────────────────┐
│ SIGNAL │
│ │
│ count = signal(0) │
│ count() ← read, synchronous │
│ count.set(1) ← write │
│ doubled = computed(() => count() * 2) │
│ │
│ Template: {{ count() }} │
│ Change detection: automatic │
│ Cleanup: automatic │
│ │
├──────────────────────────────────────────────────────────┤
│ BEHAVIORSUBJECT │
│ │
│ count$ = new BehaviorSubject(0) │
│ count$.value ← read, synchronous (escape) │
│ count$.next(1) ← write │
│ doubled$ = count$.pipe(map(v => v * 2)) │
│ │
│ Template: {{ count$ | async }} │
│ Change detection: via async pipe │
│ Cleanup: manual │
│ │
│ Signals are simpler and more integrated. │
│ BehaviorSubject remains for the RxJS pipelines. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: The Effect Lifecycle
┌──────────────────────────────────────────────────────────┐
│ effect(() => { │
│ const current = this.theme(); │
│ document.body.classList.toggle('dark', current === 'dark');│
│ return () => { /* cleanup */ }; │
│ }); │
│ │
│ CREATION │
│ The effect runs immediately. │
│ The reads are recorded. │
│ │
│ CHANGE │
│ The theme changes. │
│ The effect re-runs. │
│ │
│ DESTROY │
│ The cleanup runs. │
│ The effect is released. │
│ │
└──────────────────────────────────────────────────────────┘
Summary
| Primitive | Purpose | API |
|---|---|---|
signal | State | (), .set(), .update() |
computed | Derived | () |
effect | Side effect | effect(fn) |
| Concept | Value |
|---|---|
| Read | Function call () |
| Write | set or update |
| Derived | computed |
| Side effect | effect |
| Cleanup | onCleanup |
| Read-only | asReadonly() |
| Change detection | Automatic on template read |
| Dependency tracking | Automatic |
Key takeaways:
- A signal is a reactive value read with
()— the read is what registers the dependency, and the value is always available synchronously - A
computedis a derived value — it tracks the signals it reads, recomputes when they change, and caches the result - An
effectruns a side effect when the signals it reads change — it is the escape hatch to the imperative world, and it is for side effects, not for data fetching or state derivation - The signal must be updated immutably when it holds an object — a mutation does not change the reference, and the consumers are not notified
- The
updatemethod is the atomic way to compute from the current value —setis for a known value, andupdateis for a dependent one - The template read is what ties the signal to the change detection — a signal read only in the component class does not mark the component for check
- The
asReadonlymethod exposes a read-only view — the writable signal stays private, and the consumers cannot write - The
effectshould not write to a signal it reads — the loop is the mistake, and thecomputedis the tool for the derived value - The signal and the
BehaviorSubjectare complementary — the signal is for the state, theBehaviorSubjectis for the stream, and the two convert withtoSignalandtoObservable - The signal model is the modern Angular — it replaces the manual subscription management with the automatic tracking and cleanup, and the change detection is more efficient
Remember: The signals are three functions — signal, computed, and effect — and they form a dependency graph that tracks itself. The state is a signal, the derived value is a computed, and the side effect is an effect. The read is a function call, the write is set or update, and the change detection is automatic when the signal is read in the template. The model is the modern Angular, and the RxJS remains for the streams.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!