Signals

Angular Signals vs RxJS: Where They Overlap, Where They Complement

Compare Angular Signals and RxJS for reactive state management. Learn where they overlap, where they complement each other, and how Angular's reactive model is evolving.

8 min read
Angular Signals vs RxJS: Where They Overlap, Where They Complement
Share: X · LinkedIn

Angular Signals and RxJS are both reactive primitives, but they solve fundamentally different problems. Signals manage synchronous state and derived values. RxJS manages asynchronous event streams with operators for timing, cancellation, and orchestration. Understanding where they overlap and where they complement each other is essential for building Angular applications that are simple where possible and powerful where necessary.

What signals are designed for

Signals are Angular’s answer to synchronous reactive state. They represent a value that changes over time and automatically notify consumers when it updates.

The core signal APIs include:

  • signal() — writable reactive value
  • computed() — derived value that recalculates when dependencies change
  • effect() — side effect that runs when signal dependencies change
  • linkedSignal() — dependent state that resets when a source signal changes
  • resource() — declarative async data fetching tied to signal parameters

Signals are pull-based: the template or a computed function reads the current value when it needs it. There is no subscription management, no async pipe, and no manual teardown. Change detection in Angular uses signals to know exactly which components need to re-render.

const count = signal(0);
const doubled = computed(() => count() * 2);

// Template reads doubled() — updates automatically when count changes

Signals excel at:

  • Component-local UI state (toggles, form state, selected items)
  • Derived values across multiple sources (filtered lists, totals, validation status)
  • Replacing BehaviorSubject for simple state holders
  • Template reactivity without async pipe boilerplate

What RxJS is designed for

RxJS is a library for composing asynchronous and event-based programs using observable sequences. It provides operators for transforming, filtering, combining, and managing streams of events over time.

The core RxJS patterns in Angular include:

  • HttpClient returns observables for HTTP requests
  • switchMap / mergeMap / concatMap for flattening async operations
  • debounceTime / throttleTime for rate limiting
  • combineLatest / forkJoin for combining multiple async sources
  • retry / catchError for error recovery
  • takeUntilDestroyed for lifecycle-aware subscriptions
  • Subject / BehaviorSubject for imperative event buses

RxJS is push-based: values arrive over time, and operators transform or react to them as they flow through the stream. This is ideal for scenarios where timing, ordering, and cancellation matter.

this.searchControl.valueChanges.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(term => this.http.get<Result[]>(`/api/search?q=${term}`))
).subscribe(results => this.results.set(results));

RxJS excels at:

  • HTTP request orchestration (cancellation, retry, sequential calls)
  • User input streams (typeahead, drag events, scroll position)
  • WebSocket and real-time data streams
  • Complex async workflows (polling, race conditions, timeout handling)
  • Event coordination across multiple sources with timing constraints

Where they overlap

There is a genuine overlap between signals and RxJS in a few areas:

Simple state holders

A BehaviorSubject and a signal() both hold a current value and notify consumers on change. For this use case, signals are strictly simpler:

// Before: RxJS
private readonly count$ = new BehaviorSubject(0);
readonly count$ = this.count$.asObservable();

// After: Signals
private readonly _count = signal(0);
readonly count = this._count.asReadonly();

No subscription needed, no async pipe in the template, no memory leak risk from forgotten unsubscribes.

Derived state

Both combineLatest with map and computed() can derive values from multiple sources:

// RxJS
readonly total$ = combineLatest([this.price$, this.quantity$]).pipe(
  map(([price, qty]) => price * qty)
);

// Signals
readonly total = computed(() => this.price() * this.quantity());

For synchronous derivations, computed() is simpler and more efficient. It only recalculates when a dependency actually changes, and it integrates directly with Angular’s change detection.

Event reactions

Both effect() and tap() / subscribe() can trigger side effects in response to changes. However, effect() is limited to synchronous reactions to signal changes, while RxJS provides operators for debouncing, throttling, and async side effects.

Where they complement each other

The most effective Angular architecture uses both: RxJS for async orchestration and signals for state and template binding.

Pattern: RxJS fetches, signals store

The most common hybrid pattern is: use RxJS to manage the HTTP lifecycle (cancellation, retries, error handling), then write the result into a signal for the template:

@Injectable()
export class ArticleStore {
  private readonly http = inject(HttpClient);
  private readonly _articles = signal<Article[]>([]);
  private readonly _loading = signal(false);
  private readonly _error = signal<string | null>(null);

  readonly articles = this._articles.asReadonly();
  readonly loading = this._loading.asReadonly();
  readonly error = this._error.asReadonly();

  loadArticles(category: string) {
    this._loading.set(true);
    this._error.set(null);

    this.http.get<Article[]>(`/api/articles?cat=${category}`).pipe(
      retry({ count: 2, delay: 1000 }),
      finalize(() => this._loading.set(false))
    ).subscribe({
      next: articles => this._articles.set(articles),
      error: err => this._error.set(err.message)
    });
  }
}

The template reads signals directly — no async pipe, no subscription tracking. RxJS handles what it is good at (HTTP lifecycle), and signals handle what they are good at (reactive state for the view).

Pattern: RxJS for user input, signals for state

For typeahead search, RxJS manages debounce and cancellation while signals hold the result:

@Component({...})
export class SearchComponent {
  private readonly searchControl = new FormControl('');
  private readonly http = inject(HttpClient);

  readonly results = signal<SearchResult[]>([]);

  constructor() {
    this.searchControl.valueChanges.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      filter(term => term.length >= 2),
      switchMap(term => this.http.get<SearchResult[]>(`/api/search?q=${term}`)),
      takeUntilDestroyed()
    ).subscribe(results => this.results.set(results));
  }
}

Pattern: WebSocket streams into signals

Real-time data from a WebSocket is inherently stream-based. RxJS manages the connection and signals expose the latest state:

@Injectable({ providedIn: 'root' })
export class LivePriceService {
  private readonly _prices = signal<Map<string, number>>(new Map());
  readonly prices = this._prices.asReadonly();

  connect() {
    webSocket<PriceUpdate>('wss://prices.example.com').pipe(
      retryWhen(errors => errors.pipe(delay(5000)))
    ).subscribe(update => {
      this._prices.update(map => {
        const next = new Map(map);
        next.set(update.symbol, update.price);
        return next;
      });
    });
  }
}

How Angular’s reactive model is evolving

Angular’s roadmap shows a clear direction: signals as the primary reactive primitive for state and change detection, with RxJS remaining available for async workflows.

What has already shipped

  • Signal-based components with input(), output(), model(), viewChild(), contentChild()
  • resource() and rxResource() for declarative async data loading
  • linkedSignal() for dependent state management
  • toSignal() and toObservable() for interop between signals and observables
  • Zoneless change detection powered by signals

Where Angular is heading

  • Less reliance on RxJS for common tasks: resource() reduces the need for manual HTTP observable management. Many components will not need RxJS at all.
  • RxJS becomes opt-in, not mandatory: future Angular versions may reduce or make RxJS an optional peer dependency for teams that do not need it.
  • Signal-based forms: the Angular team has discussed signal-based form primitives that would replace or complement ReactiveFormsModule.
  • Signal-based router events: router navigation could expose signals instead of observables for common use cases.

What this means for your codebase

RxJS is not going away. It remains the best tool for complex async patterns. But the surface area where RxJS is necessary is shrinking. New Angular projects should default to signals for state and reach for RxJS only when the problem is genuinely async or stream-based.

Decision guide: when to use which

ScenarioUse SignalsUse RxJSUse Both
Component UI state (toggle, tab, selected item)Yes
Derived values (totals, filters, validation)Yes
Simple data fetch (load once, display)Yes (resource())
Search typeahead with debounceYes
HTTP with retry, cancellation, sequencingYes
WebSocket / real-time streamsYes
Cross-feature event coordinationYes
Form value tracking (reactive forms)Yes (for now)
Route parameter reactionsYes (input())
Polling with interval and stop conditionsYes

Rule of thumb

If the value exists right now and you need to read it — use a signal. If the value will arrive over time and you need to transform, filter, or coordinate it — use RxJS. If both are true, use RxJS for the stream and write the result into a signal.

FAQ

Should I stop using RxJS in Angular?

No. RxJS remains essential for async orchestration, HTTP lifecycle management, and event stream processing. What you should stop doing is using BehaviorSubject as a state holder when a signal is simpler, and using combineLatest for derived state when computed() is cleaner.

Can I use signals without RxJS at all?

For many components, yes. With resource() for data fetching and signals for state, a component may have zero RxJS imports. However, at the service or infrastructure level, RxJS is still the right tool for complex async patterns.

Is toSignal() a good long-term pattern?

toSignal() is a pragmatic bridge for converting existing observables to signals. It works well for gradual migration. In new code, prefer writing to signals directly from subscription handlers rather than wrapping entire observable chains in toSignal(), since this gives you more control over error and loading states.

Will Angular remove RxJS as a dependency?

Angular may make RxJS an optional peer dependency in future versions, but it will not remove RxJS support. Teams that rely on observables for forms, HTTP, and router events will continue to use RxJS. The shift is toward making RxJS opt-in for projects that do not need it.

What should new Angular developers learn first?

Learn signals first. They are simpler, more intuitive, and cover the majority of reactive patterns in modern Angular. Then learn RxJS fundamentals (operators, subscription lifecycle, error handling) for when you encounter async orchestration challenges that signals cannot solve alone.

Conclusion and next steps

Signals and RxJS are not competitors — they are complementary tools for different reactive problems. Signals simplify synchronous state, derived values, and template binding. RxJS manages async streams, orchestration, and timing. The best Angular applications use both intentionally: signals as the default, RxJS when the problem demands it.

Audit one feature in your application. Identify every BehaviorSubject used as a simple state holder and every combineLatest used for synchronous derivation. These are candidates for migration to signals. For a deeper look at how to structure signal-based state in enterprise teams, read Angular Signals for Enterprise State Architecture. If you are also weighing whether to keep NgRx in your stack, see NgRx: To Be or Not to Be in Modern Angular.

Previous
Angular vs Next.js: SEO, SSR, and Content Platform Comparison
Next
NgRx: To Be or Not to Be in Modern Angular