Angular Signals vs RxJS Observables: When to Use Which
You don’t have to pick one. That’s the first thing to understand. Signals didn’t replace RxJS — they replaced a very specific subset of what RxJS was doing. The problem is that most Angular code was using observables for everything, even when they were overkill.
The Core Difference
Signals hold a value. Observables push values over time.
That sounds simple, but it changes how you think about state.
"What is the current value?"
Synchronous. Pull-based. Always has a value.
"What happens next?"
Asynchronous. Push-based. May or may not have a value.
A signal is like a spreadsheet cell. You read it, you get a value. When it changes, everything that depends on it updates automatically.
An observable is like an event stream. HTTP responses, WebSocket messages, route changes — things that happen rather than things that are.
Where Signals Win
Signals are better for synchronous, component-level state. The kind of state you used to wire up with BehaviorSubject + async pipe.
Before (RxJS):
@Component({
template: `{{ count$ | async }}`
})
export class CounterComponent {
private countSubject = new BehaviorSubject(0);
count$ = this.countSubject.asObservable();
increment() {
this.countSubject.next(this.countSubject.value + 1);
}
}
After (Signals):
@Component({
template: `{{ count() }}`
})
export class CounterComponent {
count = signal(0);
increment() {
this.count.update(v => v + 1);
}
}
Less code. No subscription management. No async pipe. No BehaviorSubject ceremony. And the framework knows exactly what changed without running change detection on the entire component tree.
BehaviorSubject just to hold and update a value — that's a signal. Every time.
Where Observables Still Win
RxJS shines when you need to describe how data flows over time. Think operators, timing, and coordination.
export class SearchComponent {
private searchQuery = new Subject<string>();
results$ = this.searchQuery.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(query => this.http.get(`/api/search?q=${query}`))
);
onSearch(query: string) {
this.searchQuery.next(query);
}
}
Try doing debounceTime → distinctUntilChanged → switchMap with signals. You can’t — at least not without reinventing half of RxJS. Observables give you a composable pipeline for async operations. Signals don’t.
Here’s the rule: if you need operators, you need observables.
The Decision Flowchart
Bridging the Two
Angular gives you toSignal() and toObservable() for interop. Use them at the boundaries.
export class UserComponent {
private userService = inject(UserService);
users = toSignal(this.userService.getUsers(), { initialValue: [] });
activeCount = computed(() => this.users().filter(u => u.active).length);
}
The observable handles the HTTP call. The signal holds the result. computed() derives new state from it. Each tool does what it’s best at.
Going the other direction works too:
export class FilterComponent {
filter = signal('');
results = toSignal(
toObservable(this.filter).pipe(
debounceTime(300),
switchMap(q => this.http.get(`/api/items?q=${q}`))
),
{ initialValue: [] }
);
}
Signal for the input. Observable pipeline for the async work. Signal for the template. Clean boundaries.
The Practical Takeaway
Stop defaulting to observables for everything. Here’s the split:
| Use case | Signal | Observable |
|---|---|---|
| Component UI state | ✔ | |
| Derived/computed values | ✔ | |
| Shared app state | ✔ | |
| HTTP requests | ✔ | |
| WebSocket streams | ✔ | |
| Debounce / throttle | ✔ | |
| Complex async coordination | ✔ | |
| Form inputs → template | ✔ |
If your observable never uses an operator beyond subscribe, replace it with a signal. If your state management involves timing, cancellation, or stream composition, keep the observable.
The goal isn’t to eliminate RxJS. It’s to stop using it where you don’t need it — and let signals handle the simple stuff they were designed for.