RxJS in Angular 2026: operatori, Subjects e interop con i Signals

RxJS in Angular 2026: operatori, Subjects e i pattern di interop con i Signals che gli sviluppatori Angular usano in produzione, più le domande da colloquio più frequenti.

RxJS in Angular 2026: operatori, Subjects e interop con i Signals

RxJS in Angular resta la spina dorsale della programmazione asincrona nel 2026, anche mentre i Signals ridisegnano il modo in cui i componenti Angular tracciano lo stato sincrono. Sapere quando ricorrere a un Observable, quando un Signal si adatta meglio e come i due interoperano è ormai una competenza fondamentale per qualsiasi sviluppatore Angular. Questo approfondimento attraversa gli operatori, i Subjects e i pattern di interop che distinguono una risposta sicura in un colloquio da una che si blocca.

Signals vs Observables in una riga

I Signals modellano lo stato sincrono che ha sempre un valore corrente; gli Observables modellano flussi asincroni di eventi nel tempo. Angular 20 mantiene entrambi, e il pacchetto @angular/core/rxjs-interop è il ponte ufficiale tra i due.

Perché RxJS conta ancora accanto ai Signals di Angular

L'introduzione dei Signals ha spinto molti team a chiedersi se RxJS fosse in via di dismissione. La risposta, tre versioni maggiori dopo, è no. I Signals eccellono nel rappresentare lo stato che ha un valore in questo momento: un contatore, un campo di un form, una scheda selezionata. Gli Observables eccellono nel modellare eventi che arrivano nel tempo e possono richiedere trasformazione, cancellazione o combinazione: risposte HTTP, messaggi WebSocket, input da tastiera, eventi del router e timer.

Le stesse API di Angular rendono visibile questa divisione. HttpClient restituisce ancora Observables. I reactive form espongono valueChanges e statusChanges come Observables. Gli eventi del router scorrono attraverso un Observable. Ovunque più eventi asincroni fluiscano nello stesso canale, gli operatori RxJS restano lo strumento più espressivo a disposizione. La guida ufficiale alla reattività di Angular tratta i due modelli come complementari anziché in competizione, e la documentazione di RxJS rimane un riferimento obbligato per qualsiasi flusso di dati non banale.

La regola pratica che regge in code review: Signals per lo stato locale del componente letto nei template, e Observables per le pipeline asincrone che richiedono operatori. Lo strato di interop collega i due, così nessun modello diventa un muro.

Gli operatori di combinazione rafforzano questo punto. Coordinare più sorgenti asincrone, come fondere i parametri di rotta con i filtri salvati di un utente e un feed di prezzi in tempo reale, è esattamente ciò per cui combineLatest, withLatestFrom e forkJoin sono stati costruiti. I Signals possono derivare valori calcolati da altri signals, ma non possono esprimere una coordinazione basata sul tempo come "emetti solo dopo che tutte e tre le sorgenti hanno prodotto almeno un valore". Quella composizione dichiarativa nel tempo è la ragione duratura per cui RxJS resta nella cassetta degli attrezzi.

Operatori RxJS fondamentali su cui ogni sviluppatore Angular fa affidamento

Gli operatori sono funzioni pure che prendono un Observable e ne restituiscono uno nuovo. Lo scenario classico da colloquio è la ricerca durante la digitazione, perché mette all'opera quattro operatori contemporaneamente: limitare l'input, saltare i duplicati, cancellare le richieste obsolete e recuperare dagli errori.

search.component.tstypescript
import { Component, inject } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { debounceTime, distinctUntilChanged, switchMap, catchError } from 'rxjs/operators';
import { of } from 'rxjs';

@Component({
  selector: 'app-search',
  imports: [ReactiveFormsModule],
  template: `<input [formControl]="query" placeholder="Search repositories" />`,
})
export class SearchComponent {
  private http = inject(HttpClient);
  query = new FormControl('');

  results$ = this.query.valueChanges.pipe(
    debounceTime(300),               // wait until typing pauses for 300ms
    distinctUntilChanged(),          // ignore emissions that repeat the last value
    switchMap((term) =>              // cancel the in-flight request, start a fresh one
      this.http.get(`/api/search?q=${term}`).pipe(
        catchError(() => of([]))     // recover from a failed request without killing the stream
      )
    )
  );
}

Il dettaglio sottile che gli intervistatori sondano è perché switchMap si trova al livello esterno mentre catchError è annidato al suo interno. Collocare catchError sull'Observable HTTP interno significa che una richiesta fallita restituisce un array vuoto e il flusso esterno continua a scorrere. Spostare catchError sulla pipe esterna terminerebbe l'intera ricerca dopo il primo errore, rompendo ogni pressione di tasto successiva. I quattro operatori di flattening rispondono ciascuno a una diversa domanda di concorrenza, e scegliere quello sbagliato è una delle fonti più comuni di race condition nelle app Angular.

Scegliere un operatore di flattening

Usa switchMap per cancellare la richiesta interna precedente e mantenere solo l'ultima (ricerca typeahead). Usa mergeMap per eseguire ogni richiesta interna in concorrenza (upload paralleli indipendenti). Usa concatMap per accodare le richieste in ordine rigoroso (scritture sequenziali che non devono sovrapporsi). Usa exhaustMap per ignorare i nuovi input finché quello corrente non termina (bloccare un pulsante di invio cliccato due volte).

Azzeccare questa scelta è ciò che trasforma una funzionalità instabile in una prevedibile. Il modulo SharpSkill sugli operatori RxJS allena queste distinzioni con domande a punteggio che riproducono le esatte race condition che ciascun operatore previene.

Subjects e multicasting: BehaviorSubject, ReplaySubject e Subject

Un Observable semplice è unicast: ogni subscriber innesca un'esecuzione nuova. Un Subject è al contempo un Observable e un Observer, il che lo rende multicast. Questa proprietà fa dei Subjects il modo standard per costruire uno store di stato leggero o un bus di eventi all'interno di un service.

Le tre varianti differiscono per ciò che riproducono ai subscriber tardivi. Un Subject semplice non emette nulla dal passato, quindi un subscriber vede solo i valori che arrivano dopo che si è iscritto. Un BehaviorSubject memorizza il valore corrente e lo riproduce immediatamente, ed è per questo che richiede un valore iniziale. Un ReplaySubject bufferizza un numero configurabile di emissioni passate e le riproduce tutte.

cart.service.tstypescript
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class CartService {
  // BehaviorSubject holds the latest value and replays it to new subscribers
  private readonly itemsSubject = new BehaviorSubject<string[]>([]);

  // expose a read-only stream, never the writable Subject itself
  readonly items$ = this.itemsSubject.asObservable();

  add(item: string): void {
    const current = this.itemsSubject.value;    // synchronous access to the last value
    this.itemsSubject.next([...current, item]); // emit the next immutable state
  }
}

Qui contano due abitudini. Primo, esporre asObservable() anziché il Subject impedisce al codice esterno di chiamare next() e mutare lo stato dalla porta di servizio. Secondo, emettere un nuovo riferimento di array invece di mutare in loco mantiene prevedibili la change detection e i controlli di uguaglianza. Questo pattern precede i Signals e funziona ancora bene per lo stato condiviso e iniettabile che più componenti tra loro slegati consumano.

Detto ciò, un BehaviorSubject usato puramente per tenere un singolo valore corrente letto nei template oggi si esprime spesso meglio come Signal scrivibile, che rimuove la sottoscrizione e la cerimonia di asObservable(). Il Subject resta la scelta più forte quando lo store deve anche trasformare, debounce o combinare le sue emissioni con altri flussi, dato che quegli operatori non hanno equivalente diretto tra i Signals. Decidere tra i due caso per caso, anziché ripiegare ovunque su uno, è un segno di padronanza che gli intervistatori premiano.

Interop RxJS-Signals con toSignal e toObservable

Il pacchetto di interop offre due funzioni di punta. toSignal converte un Observable in un Signal di sola lettura che un template può consumare direttamente, e gestisce automaticamente il ciclo di vita della sottoscrizione. toObservable fa l'inverso, trasformando un Signal in un Observable così che gli operatori RxJS possano elaborarne i cambiamenti.

dashboard.component.tstypescript
import { Component, signal, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { switchMap } from 'rxjs/operators';

@Component({ selector: 'app-dashboard', template: `{{ user()?.name }}` })
export class DashboardComponent {
  private http = inject(HttpClient);

  // a Signal drives the query; toObservable turns each change into a stream event
  readonly userId = signal(1);
  private readonly user$ = toObservable(this.userId).pipe(
    switchMap((id) => this.http.get<{ name: string }>(`/api/users/${id}`))
  );

  // toSignal subscribes immediately and unsubscribes on destroy, no manual teardown
  readonly user = toSignal(this.user$, { initialValue: null });
}

L'opzione initialValue merita attenzione. Senza di essa, toSignal restituisce un Signal tipizzato come il valore oppure undefined, perché nulla ha ancora emesso. Passare initialValue: null dà al template uno stato iniziale definito e un tipo più pulito. Per sorgenti sincrone come un BehaviorSubject, requireSync: true garantisce che un valore sia disponibile alla prima lettura ed elimina del tutto undefined dal tipo. Questo ponte bidirezionale è ciò che permette a una codebase di adottare i Signals in modo incrementale senza riscrivere ogni pipeline Observable esistente. Il modulo di colloquio sui Signals di Angular copre in profondità il modello di reattività, e la panoramica delle funzionalità dei Signals in Angular 18 ricostruisce come queste primitive si sono evolute.

Pronto a superare i tuoi colloqui su Angular?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Collegare gli Observables alle signal resource con rxResource

Angular 20 promuove rxResource come il modo dichiarativo per caricare dati asincroni da un Observable in uno stato basato su signal. Traccia lo stato di caricamento, errore e risoluzione come signals, il che rimuove gran parte della contabilità manuale delle sottoscrizioni che le chiamate HTTP richiedevano un tempo.

products.component.tstypescript
import { Component, signal, inject } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';

@Component({ selector: 'app-products', template: `` })
export class ProductsComponent {
  private http = inject(HttpClient);
  readonly category = signal('laptops');

  // Angular 20 renamed the fields: request -> params and loader -> stream
  readonly products = rxResource({
    params: () => ({ category: this.category() }),
    stream: ({ params }) =>
      this.http.get<Product[]>(`/api/products?category=${params.category}`),
  });
  // products.value(), products.isLoading() and products.error() are all signals
}

La rinomina da request/loader in Angular 19 a params/stream in Angular 20 è un inciampo frequente, quindi menzionare i nomi attuali dei campi segnala conoscenze aggiornate in un colloquio. Quando il signal category cambia, rxResource cancella la richiesta precedente e avvia un nuovo stream, offrendo gratuitamente la semantica di switchMap. Il riferimento all'API rxResource documenta l'intero ciclo di vita dello stato.

Evitare i memory leak con takeUntilDestroyed

Qualsiasi subscribe() manuale che sopravvive al proprio componente causa un memory leak e può far scattare callback contro una view distrutta. La correzione storica era il pattern takeUntil(this.destroy$) con un Subject completato in ngOnDestroy. Angular ha sostituito quel boilerplate con takeUntilDestroyed, un operatore che lega la sottoscrizione all'injection context corrente o a un DestroyRef esplicito.

live-feed.component.tstypescript
import { Component, inject, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';

@Component({ selector: 'app-live-feed', template: `` })
export class LiveFeedComponent {
  private destroyRef = inject(DestroyRef);

  constructor() {
    interval(1000)
      // completes the subscription automatically when the component is destroyed
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe((tick) => console.log('tick', tick));
  }
}

Chiamato dentro un costruttore o un inizializzatore di campo, takeUntilDestroyed() non necessita di argomenti perché legge il DestroyRef ambientale. Chiamato altrove, come dentro un metodo, richiede un DestroyRef esplicito passato in ingresso, come mostrato sopra. Preferire toSignal e rxResource evita del tutto le sottoscrizioni manuali, che è il modo più pulito per aggirare i leak; quando serve davvero una sottoscrizione grezza, takeUntilDestroyed è la guardia corretta.

Il tranello dell'injection context

Chiamare takeUntilDestroyed() senza argomenti fuori da un injection context lancia un errore a runtime. Sottoscrivere dentro un event handler, un setTimeout o una callback RxJS è fuori da quel context, quindi il DestroyRef va catturato prima (tipicamente come campo iniettato) e passato esplicitamente. Dimenticarlo è la causa più frequente dell'errore "takeUntilDestroyed() can only be used within an injection context".

Domande da colloquio su Angular e RxJS che vale la pena ripassare

Gli intervistatori tendono a girare attorno agli stessi argomenti ad alto segnale. Saper rispondere a questi in modo netto separa i candidati che hanno usato RxJS da quelli che ne hanno solo letto.

  • Observables hot contro cold. Un Observable cold avvia il suo producer per ogni sottoscrizione, quindi ogni subscriber ottiene un'esecuzione indipendente (una richiesta HTTP nuova). Un Observable hot condivide un solo producer tra i subscriber, ed è ciò che creano i Subjects e share().
  • switchMap contro mergeMap contro concatMap contro exhaustMap. switchMap cancella l'Observable interno precedente (ideale per la ricerca). mergeMap esegue tutti gli Observable interni in concorrenza. concatMap li accoda in ordine. exhaustMap ignora i nuovi input mentre uno è in esecuzione (ideale per prevenire i doppi invii di form).
  • Come prevenire i memory leak. Preferire toSignal, rxResource o la pipe async, che si disiscrivono tutte automaticamente; per le sottoscrizioni manuali, usare takeUntilDestroyed.
  • Quando convertire un Observable in un Signal. Ricorrere a toSignal quando un template ha bisogno di un valore in modo sincrono e la sorgente è asincrona, lasciando che la change detection legga l'ultimo valore senza una pipe async.

Lavorare su versioni a punteggio di queste sotto pressione di tempo è il modo più rapido per interiorizzarle. L'hub tecnologico Angular raggruppa i moduli su RxJS, Signals e change detection che mappano direttamente su queste domande.

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Conclusione

RxJS e Signals sono partner nello stack Angular moderno, non rivali. Le conclusioni che vale la pena portarsi nel prossimo progetto o colloquio:

  • Usare i Signals per lo stato sincrono del componente e gli Observables per le pipeline di eventi asincroni che richiedono operatori.
  • Ricorrere a switchMap per cancellare il lavoro obsoleto, a exhaustMap per bloccare i duplicati, e tenere catchError sull'Observable interno così che il flusso esterno sopravviva ai fallimenti.
  • Esporre lo stato dei Subject tramite asObservable() ed emettere valori immutabili per mantenere prevedibile la change detection.
  • Collegare i due modelli con toSignal e toObservable, e passare initialValue o requireSync per tipi puliti.
  • Caricare dati asincroni in modo dichiarativo con rxResource in Angular 20, ricordando i nomi dei campi params e stream.
  • Proteggere ogni sottoscrizione manuale residua con takeUntilDestroyed per eliminare i leak.

Tag

#angular
#rxjs
#signals
#observables
#typescript
#deep-dive

Condividi

Articoli correlati