Skip to main content
Angular Signals vs RxJS: Choosing the Right Tool for the Job

Angular Signals vs RxJS: Choosing the Right Tool for the Job

One of the most repetitive Angular debates right now is also one of the least useful:

Should we use Signals or RxJS?

In practice, that is usually the wrong question.
The real question is: what kind of problem are you solving?

Because these two tools are not doing the same job.

Signals are excellent for representing current state and derived state inside Angular. RxJS is still the better tool when you are dealing with asynchronous streams, time, events, cancellation, or operator-heavy flows.

If you treat this like an ideological choice, the code gets worse.
If you treat it like a design decision, the choice becomes much simpler.

Start with the problem, not the syntax

Before picking an API, classify the problem.

Most reactive code in Angular falls into one of these buckets:

  • local UI state
  • derived state
  • async loading
  • event streams over time
  • cross-stream orchestration

That classification already tells you a lot.

If the code is mostly about holding a value and deriving other values from it, Signals are usually the cleanest option.

If the code is mostly about reacting to events over time, especially with debounce, switch, retry, merge, or cancellation, RxJS is usually the right tool.

That is the split.

What Signals are best at

Signals are best when your code needs a clear, synchronous representation of state.

Things like:

  • selected filters
  • panel open/closed state
  • current sort order
  • derived labels
  • filtered collections
  • UI flags based on other values

This kind of code tends to read very naturally with Signals because it is really just state plus derivation.

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

@Component({
  selector: 'app-product-filters',
  template: `...`
})
export class ProductFiltersComponent {
  readonly search = signal('');
  readonly category = signal<'all' | 'books' | 'tools'>('all');

  readonly hasFilters = computed(() => {
    return this.search().trim().length > 0 || this.category() !== 'all';
  });
}

This is where Signals feel right. No subscriptions, no stream vocabulary, no unnecessary ceremony.

They are also a very good fit for derived state that should never be manually synchronized. That is one of the biggest practical wins. A lot of older Angular code used Subjects and combinations of streams for things that were really just computed values.

What RxJS is still best at

RxJS is still the better tool when the problem is not “what is the current value?” but “what is happening over time?”

That includes:

  • search input with debounce
  • request cancellation
  • websockets
  • router events
  • polling
  • retries and recovery
  • merging multiple async sources
  • sequencing async work

This is where RxJS still earns its place very easily.

A good example is search with remote data. The moment you need debounce and switchMap, you are already in RxJS territory.

readonly users$ = this.searchControl.valueChanges.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(query => this.http.get<User[]>('/api/users', { params: { query } }))
);

Trying to force this into a Signals-only shape usually makes the code more awkward, not more modern.

That is an important point: newer does not automatically mean better for every use case.

The cleanest approach in real apps

In practice, the nicest Angular codebases now usually use both.

A common pattern is:

  • use RxJS for the async pipeline
  • convert to a Signal when the template or component state wants a current value

That gives you the best of both worlds.

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

That boundary is often where things click into place.

You keep RxJS where stream semantics matter, and you keep Signals where Angular state reads better as state.

The reverse also happens sometimes. You may model local state with Signals, then expose it as an Observable because one specific workflow needs RxJS operators.

That is fine too.

Interop is not a compromise. It is often the right architecture.

Decision matrix by use case

Here is the practical version.

Use Signals by default for:

  • local component state
  • derived UI state
  • filtered or sorted view models
  • synchronous state read by templates
  • replacing overly complicated BehaviorSubject setups for simple state

Use RxJS by default for:

  • user input streams with debounce
  • request cancellation
  • websockets and server-sent events
  • router event processing
  • retry logic
  • combining or flattening multiple async streams
  • anything where time and event order matter

Use both when:

  • the source is a stream, but the view wants current state
  • the source is signal-based state, but a downstream workflow needs RxJS operators

That is usually enough to make a good decision quickly.

Anti-patterns to avoid

This is where teams usually get themselves into trouble.

1. Using effects to propagate state

If one value is derived from another, use computed, not an effect that writes into another signal.

Bad:

effect(() => {
  this.fullName.set(`${this.firstName()} ${this.lastName()}`);
});

Better:

readonly fullName = computed(() => `${this.firstName()} ${this.lastName()}`);

If you are using effects to keep state in sync, there is a good chance the model is off.

2. Rebuilding RxJS badly with Signals

Signals are not meant to replace every stream operator. If you are manually recreating debounce, cancellation, or merging logic with effects and timers, you are fighting the problem instead of solving it.

3. Keeping everything in RxJS out of habit

This is still common in older Angular codebases. Simple UI state ends up hidden behind Subjects and operator chains even when it would be much clearer as a couple of signals and a computed value.

Sometimes that is not architecture. It is just leftover habit.

My default rules

If I were setting a baseline for a modern Angular project today, it would be this:

  1. Start with Signals for component-facing state
  2. Use RxJS when the problem involves time, events, or async orchestration
  3. Convert at boundaries, not everywhere
  4. Prefer computed state over synchronization logic
  5. Use effects for real side effects, not for building state graphs

These rules are simple, but they hold up well in real projects.

Suggested articleWhen Micro-Frontends Make Sense in Angular - and When They Don’t Angular Architecture When Micro-Frontends Make Sense in Angular - and When They Don’tA practical Angular guide to micro-frontends: when they fit, what they cost, and how to decide if your teams really need them.

About

A little more about me

I help teams build stronger Angular products through clearer architecture, better frontend execution, and more confident technical decisions. The ideas in these articles come from real work with client teams, real delivery challenges, and practical solutions that proved useful in the field.

Need help with Angular delivery?

If your team needs clearer direction, stronger frontend execution, or a second expert opinion, send me a short message and I will suggest the most useful next step.