← Back to blog

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.

Signal
"What is the current value?"
Synchronous. Pull-based. Always has a value.
Observable
"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.

Key insight: If you're reaching for 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 debounceTimedistinctUntilChangedswitchMap 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

Do you need to transform a stream over time?
Yes → Observable
HTTP, WebSockets, debounce, retry, combine streams
No → Signal
UI state, toggles, counters, derived/computed values

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.