Angular Signals: Building Reactive Applications Without Overusing RxJS
15 Aug 2026 · 8 min read · Intermediate
- #Angular
- #Signals
- #RxJS
- #Architecture
Share
Signals give Angular a first-class reactive primitive for synchronous state. They do not replace RxJS. The useful skill is knowing when each tool wins.
Why Signals?
RxJS shines for events over time: WebSockets, search-as-you-type, complex orchestration. For “this value changed, update the template,” Signals are simpler:
- No subscription lifecycle to manage in components
- Fine-grained updates with
computed() - Clear mental model for derived UI state
import { signal, computed, effect } from '@angular/core';
const count = signal(0);
const doubled = computed(() => count() * 2);
effect(() => {
console.log('count is', count());
});
count.set(1);
count.update((n) => n + 1);Signals vs RxJS
| Concern | Prefer |
|---|---|
| Component / local UI state | Signal |
| Derived values | computed() |
| HTTP once, then display | Signal (or resource APIs) |
| Debounced input streams | RxJS |
| Multiplexed async events | RxJS |
A practical rule: own state with Signals; integrate the world with Observables.
Bridging with toSignal and toObservable
When a service already returns an Observable, convert at the boundary:
import { inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { UserService } from './user.service';
export class ProfileComponent {
private readonly users = inject(UserService);
readonly profile = toSignal(this.users.currentUser$, {
initialValue: null,
});
}Going the other direction (toObservable) is useful when a library API still expects streams.
Practical component pattern
Keep writable Signals private when possible, and expose read-only views:
import { Component, computed, signal } from '@angular/core';
@Component({
selector: 'app-cart-summary',
template: `
<p>{{ itemCount() }} items</p>
<p>Total: {{ total() | number: '1.2-2' }}</p>
`,
})
export class CartSummaryComponent {
private readonly items = signal<{ price: number }[]>([]);
readonly itemCount = computed(() => this.items().length);
readonly total = computed(() =>
this.items().reduce((sum, item) => sum + item.price, 0),
);
addItem(price: number): void {
this.items.update((list) => [...list, { price }]);
}
}Performance considerations
- Prefer
computed()over recreating values in the template. - Avoid writing to Signals inside
effect()unless you understand the dependency graph—feedback loops are easy to create. - Large immutable updates still cost; Signals do not magically make huge arrays cheap.
Conclusion
Use Signals for reactive UI state and keep RxJS for asynchronous pipelines. Treat interoperability helpers as adapters at the edges, not as a reason to wrap every Signal in an Observable.