All posts

Angular Signals and RxJS are not enemies

Angular Signals and RxJS are not enemies. They are different tools with different strengths. In modern Angular applications, the best architecture often uses both.

A Signal represents current state. An Observable represents values over time. That single distinction solves most of the confusion.

If you are storing UI state, selected tabs, local filters, toggles, cart totals, or derived values, Signals are usually the cleanest solution.

If you are handling async workflows, HTTP cancellation, debounced search, router streams, WebSocket messages, retries, polling, or multiple event sources, RxJS is usually the better solution.


Mental model

A Signal answers the question: “What is the value right now?”

An Observable answers the question: “What values are emitted over time?”

Signals are synchronous, state-oriented and very readable in templates.

RxJS is asynchronous, stream-oriented and extremely powerful when time matters.

Use case Best choice Reason
Open or close a menu Signals Simple local UI state
Filter an already loaded list Signals Derived synchronous state
Debounced backend search RxJS Time, cancellation and async flow
Route parameter changes RxJS plus Signals Stream from router, current value for UI
Shopping cart totals Signals Derived values from current state
Live notifications RxJS plus Signals Streaming source, latest value in UI

What Angular Signals are good for

Signals are excellent when your component or feature needs a clear state model.

They reduce boilerplate compared to using BehaviorSubject for every small piece of state.

They also make derived state very explicit through computed.

import { computed, signal } from '@angular/core';

const price = signal(100);
const quantity = signal(2);

const total = computed(() => price() * quantity());

quantity.set(3);

console.log(total()); // 300

The total is not manually updated. It is derived from other signals.

Good Signal scenarios

  • Current user interface state.
  • Open or closed dialogs.
  • Selected filters.
  • Current theme.
  • Shopping cart items.
  • Derived totals.
  • Feature-level state stores.
  • Template-friendly state.

What RxJS is good for

RxJS shines when values are not just state, but events happening over time.

Search input is a perfect example. The user types, pauses, types again, and each value may trigger an HTTP request. You often need debounce, distinct checks and cancellation.

readonly results$ = this.searchControl.valueChanges.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap((query) => this.api.searchProducts(query))
);

This is exactly the type of problem RxJS was designed for.

Good RxJS scenarios

  • HTTP streams.
  • Debounced input.
  • Request cancellation.
  • Router events.
  • Reactive form changes.
  • WebSocket messages.
  • Polling.
  • Retry strategies.
  • Combining multiple async sources.

Real example: local product filters with Signals

Suppose an e-commerce page loads products once and then lets the user filter them locally. Signals are a very good fit.

import { computed, signal } from '@angular/core';

interface Product {
  id: number;
  name: string;
  category: string;
  stock: number;
}

const products = signal<Product[]>([]);
const query = signal('');
const selectedCategory = signal<string | null>(null);

const filteredProducts = computed(() => {
  const q = query().toLowerCase().trim();
  const category = selectedCategory();

  return products().filter((product) => {
    const matchesQuery = product.name.toLowerCase().includes(q);
    const matchesCategory = !category || product.category === category;

    return matchesQuery && matchesCategory;
  });
});

This is clean because the filtered list is simply derived from current state.


Real example: backend search with RxJS

Now imagine the list is not local. Every search must call the server. RxJS becomes the better choice.

readonly results$ = this.searchControl.valueChanges.pipe(
  startWith(this.searchControl.value),
  map((value) => value.trim()),
  debounceTime(300),
  distinctUntilChanged(),
  switchMap((query) => {
    if (query.length < 2) {
      return of([]);
    }

    return this.api.searchProducts(query).pipe(
      catchError(() => of([]))
    );
  })
);

switchMap is important because it cancels the previous request when a newer value arrives.

This kind of cancellation is one of the strongest reasons to keep RxJS in Angular projects.


Using RxJS and Signals together

You do not need to choose only one. A common pattern is to keep the async pipeline in RxJS and expose the latest result as a signal.

import { toSignal } from '@angular/core/rxjs-interop';

readonly results = toSignal(this.results$, {
  initialValue: [] as Product[]
});

This gives you the power of RxJS and the template readability of Signals.


Real example: route detail page

Route params are streams. The loaded entity is current UI state. Combining RxJS and Signals is a natural solution.

private readonly product$ = this.route.paramMap.pipe(
  map((params) => params.get('id')),
  distinctUntilChanged(),
  switchMap((id) => {
    if (!id) {
      return of(null);
    }

    return this.api.getProductById(id).pipe(
      catchError(() => of(null))
    );
  })
);

readonly product = toSignal(this.product$, {
  initialValue: null as Product | null
});

The router and HTTP flow remain Observable-based. The template reads product().


Common mistakes

Replacing all RxJS with Signals

This usually makes async code worse. RxJS is still the right tool for streams.

Using RxJS for tiny UI state

A local boolean does not need a Subject. A signal is simpler.

Putting complex HTTP logic inside effects

Effects are useful for side effects, but RxJS pipelines are clearer for debounce, cancellation and retry.

Converting everything without reason

If an Observable works perfectly with the async pipe, you may not need toSignal.


Practical decision checklist

  • Use Signals for current state.
  • Use computed for derived state.
  • Use RxJS for async streams.
  • Use RxJS for debounce.
  • Use RxJS for cancellation.
  • Use RxJS for WebSocket streams.
  • Use Signals for template-friendly feature state.
  • Use toSignal when an Observable result should become current UI state.
  • Use toObservable when a signal must enter an RxJS pipeline.

Signals make Angular state management simpler, especially for local state, derived values and feature stores.

RxJS remains essential for asynchronous programming, event streams, request cancellation, WebSocket communication and complex orchestration.

The best Angular codebase does not blindly choose Signals or RxJS. It uses Signals for state and RxJS for streams.