Signals Without the RxJS War

Signals are excellent for current state and synchronous derivation, and shipped stable in Angular v20 (May 2025). RxJS is still the better language for debounce, cancellation, polling, retry, and events over time. The boundary is the design decision; the rest is consequence.

The wrong question

The least useful version of this conversation is asking whether Signals replace RxJS. That question makes teams rewrite Observable contracts or force async workflows into Signals just to look current.

The better question is: what kind of problem is this state solving? If it is a current value or a synchronous derivation, Signals usually fit better. If it is a stream over time with debounce, cancellation, retry, polling, or WebSocket semantics, RxJS is still the tool I want.

Alex Rickabaugh (Angular team) framed the line the same way on JS Party episode 310: 'Signals are values that can change over time, and RxJS observables really give you notifications of events happening at a specific point in time.' The framework's own team teaches Signals and RxJS as different primitives, not competing ones.

The split I use

SituationDefault choiceReason
Active tab, filter text, expanded rowSignalLocal UI state with one current value.
Derived label, permission flag, filtered countcomputedSynchronous derivation from other values.
Existing user$ or permissions$ servicetoSignal at the component boundaryKeep the service contract and simplify template reads.
Autocomplete with debounce and cancellationRxJS pipeline plus Signal for UI stateTime and cancellation belong in the stream.
NgRx or ComponentStore already coordinating a featureBothKeep the store backbone; expose Signals where they help consumption.

The boundary I like

A useful team policy is simple enough to remember in code review: use Signals for local and derived component state; keep RxJS for streams, async composition, events over time, NgRx integration, and cancellation-heavy workflows.

Interop earns its place when it makes the boundary explicit via toSignal and toObservable. It is not a mandate to convert the whole application. A component can read a user Signal derived from user$ while the authentication service keeps exposing Observables because that is still the right contract for the rest of the app.

When the Angular team communicates adoption, it tends to communicate with production data. In the v20 announcement, YouTube reported a 35% improvement in interaction latency on Living Room after switching to Angular Signals via Wiz. That result lands when the team treats Signals as the right primitive for UI state, not when it tries to retrofit Observable state into Signal shape.

In the search example, normalize the query before distinctUntilChanged(). Otherwise angular and angular become different requests, and the stream is doing avoidable work.

Observable service, Signal component boundaryts
export class HeaderComponent {
  private readonly auth = inject(AuthService);

  readonly user = toSignal(this.auth.user$, { initialValue: null });
  readonly canAdmin = computed(() =>
    this.user()?.roles.includes('admin') ?? false
  );
}
Signal input, RxJS timingts
private readonly api = inject(SearchApi);
readonly query = signal('');
private readonly query$ = toObservable(this.query);

readonly results = toSignal(
  this.query$.pipe(
    map(query => query.trim()),
    debounceTime(300),
    distinctUntilChanged(),
    switchMap(term => {
      if (!term) return of([]);

      return this.api.search(term).pipe(
        catchError(() => of([]))
      );
    })
  ),
  { initialValue: [] }
);

The trap

Pawel Kozlowski (Angular team, RFC Signals author) put the distinction concretely on JS Party 310: 'A signal — I can always look at this box, and I'm going to get the value. If I subscribe to an event emitter, there was no event fired, I don't have a value.' That distinction is what makes effect the wrong tool for keeping two Signals in sync.

effect is not the new subscribe for everything. The smell is an effect that exists only to keep another Signal in sync. Use it when reactive state needs to touch an external system: localStorage, analytics, a canvas, or an imperative library. If the rule is derivation, prefer computed. If the rule is time, prefer RxJS.

Reusable artifact

Signals/RxJS boundary rule

  • Value now: Signal.
  • Value derived now: computed.
  • Events over time: RxJS.
  • Legacy Observable consumed by a component: toSignal at the boundary.
  • External side effect from reactive state: effect, sparingly.

Sources checked

Modern Angular Playbook

This article is one play.

The Modern Angular Playbook collects the diagnostic, the adoption matrix, eleven plays, and the 30-day plan. Free, in English and Portuguese.

You get both PDFs by email, through the Dojo IA list.

Open playbook →

André Ramosavailable for remote roles, UTC−3Get in touch →

Related

Guide · 9 min

HTTP State Without Turning Everything Into Signals

A code-heavy guide to Angular HTTP state boundaries with HttpClient, RxJS, toSignal, and cautious httpResource experiments.

Read article →

Error · 7 min

NG0950: Reading a Required Input Before Angular Sets It

Why Angular's NG0950 fires when a required signal input is read before binding, when inputs actually become available, and the three fixes: read it in ngOnInit / computed / effect, give it a default, or bind it in the parent.

Read article →

Note · 6 min

What to Adopt, Spike, or Wait On in Angular 21

An adoption filter for Angular 21: what belongs in code already changing, what deserves a branch, and what should stay away from critical flows.

Read article →