Let’s break down the differences between Angular Signals and Observables in depth, and provide examples so you get both the conceptual clarity and practical understanding.
Overview: Signals vs Observables
Feature | Signals | Observables |
Origin | Angular 16 (Reactively inspired) | RxJS (ReactiveX library) |
Type | Pull-based reactive primitive | Push-based reactive stream |
Use case | Simple state management, fine-grained reactivity | Complex asynchronous streams like HTTP, WebSockets |
Change detection | Integrated with Angular’s change detection | Requires manual async pipe or subscribe |
Lifecycle | Synchronous | Asynchronous |
Memory | Lightweight | Can cause memory leaks if not unsubscribed |
Interop | Native Angular APIs | External (RxJS), but tightly integrated |
Subscription management | Automatic | Manual (or async pipe) |
Learning curve | Easy for beginners | Steeper due to stream composition |
Conceptual Differences between Signals and Observables
Signals – Pull-based (Synchronous)
A Signal is like a reactive variable. You read from it like a function and Angular tracks all reads and automatically re-runs the dependent computations when values change.
Think of it like Excel: when a cell (signal) changes, all dependent cells update automatically.
Observables – Push-based (Asynchronous)
An Observable is an object that can stream various data or events that can be observed over time. You subscribe to it and it emits data (can be async). Great for event streams, API responses, user interactions, etc.
Think of it like a news subscription: you get notified (pushed) when something happens.
Basic Example
Using Signals
import { signal, computed, effect } from '@angular/core';
const count = signal(0);
const doubleCount = computed(() => count() * 2);
// Automatically re-runs when count changes
effect(() => { console.log('Double Count:', doubleCount());
});
Change signal
count.set(2); // console: Double Count: 4
count.update(n => n + 3); // console: Double Count: 10
- signal() creates reactive state
- computed() derives values
- effect() tracks dependencies and re-runs when needed
Using Observables
import { of } from 'rxjs';
import { map } from 'rxjs/operators';
const count$ = of(2); // Observable emitting 2
const doubleCount$ = count$.pipe( map(count => count * 2)
);
doubleCount$.subscribe(value => { console.log('Double Count:', value); // Output: 4
});
Key Observations:
- Observables use async operations by default (even if you use of)
- You pipe operations and subscribe to get values
Real-world Angular Usage
With Signals (State Management)
@Component({ standalone: true, template: ` <button (click)="increment()">Increment</button> <p>Count: {{ count() }}</p> `
})
export class CounterComponent { count = signal(0); increment() { this.count.update(c => c + 1); }
}
- No async pipe needed
- Just use count() to access
With Observables (Async data)
@Component({ template: ` <ul> <li *ngFor="let user of users$ | async">{{ user.name }}</li> </ul> `
})
export class UserComponent { users$: Observable<User[]>; constructor(private http: HttpClient) { this.users$ = this.http.get<User[]>('/api/users'); }
}
Key Takeaways:
- Perfect for asynchronous data like HTTP
- Requires | async pipe
Summary Table
Feature | Signals | Observables |
Data flow | Pull (read when needed) | Push (listen for emissions) |
Async support | Not inherently async | Built for async |
Dependencies | Tracked automatically | Manual management |
Lifecycle management | No cleanup needed | Needs unsubscribe or async pipe |
Use cases | UI state, forms, signals store | HTTP, WebSockets, time intervals |
When to Use What?
Scenario | Recommended |
UI state, local component state | Signals |
Derived values from other variables | Signals + Computed |
HTTP requests, intervals, event streams | Observables |
Complex stream composition (merge, debounce, retry) | Observables |
Fine-grained reactivity & simple updates | Signals |
Interop Example: Convert Observable to Signal
import { toSignal } from '@angular/core/rxjs-interop';
constructor(http: HttpClient) { this.userSignal = toSignal(http.get<User>('/api/user'));
}
- Use toSignal() to connect async Observable to Signal
Conclusion
- Use Signals for synchronous state & reactivity inside Angular templates and components.
- Use Observables for asynchronous operations and stream-based data handling.
- They can co-exist and even convert between each other when needed.