Clement Yuen
← Back to articles
Angular·Intermediate

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
Typescript
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

ConcernPrefer
Component / local UI stateSignal
Derived valuescomputed()
HTTP once, then displaySignal (or resource APIs)
Debounced input streamsRxJS
Multiplexed async eventsRxJS

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:

Typescript
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:

Typescript
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.

Related Articles