Semnalele nu înlocuiesc RxJS — îl completează. Semnalele sunt excelente pentru stare (ce e afișat acum), RxJS pentru evenimente în timp (ce se întâmplă și când). Pachetul @angular/core/rxjs-interop are punțile dintre cele două lumi. Nou cu Observables? Începe cu Observables de la zero.
| Semnale | RxJS / Observable |
|---|---|
| Au mereu o valoare curentă | Emit valori în timp (pot să nu fi emis încă) |
Sincron: count() | Asincron: subscribe() |
| Nu necesită dezabonare | Necesită dezabonare (sau operator care completează) |
Derivare: computed() | Derivare: map, combineLatest, switchMap… |
| Ideal pentru template și stare UI | Ideal pentru websocket, drag, retry, debounce complex |
toSignal(): Observable → Signal
import { toSignal } from '@angular/core/rxjs-interop';
export class Clock {
// Abonare automată + dezabonare automată la distrugerea componentei
readonly seconds = toSignal(interval(1000), { initialValue: 0 });
// Fără initialValue → tipul e T | undefined
readonly user = toSignal(this.auth.user$); // Signal<User | undefined>
// requireSync: observable-ul emite sincron (BehaviorSubject, startWith) → fără undefined
readonly theme = toSignal(this.theme$, { requireSync: true }); // Signal<Theme>
// Parametri de rută ca semnal (dacă nu folosești withComponentInputBinding)
private readonly route = inject(ActivatedRoute);
readonly id = toSignal(this.route.paramMap.pipe(map((p) => Number(p.get('id')))), {
initialValue: 0,
});
}toObservable(): Signal → Observable
import { toObservable } from '@angular/core/rxjs-interop';
export class ProductSearch {
readonly query = signal('');
// semnal → observable → operatori RxJS → înapoi la semnal
readonly results = toSignal(
toObservable(this.query).pipe(
debounceTime(300),
distinctUntilChanged(),
filter((q) => q.length >= 2),
switchMap((q) => this.api.search(q).pipe(catchError(() => of([])))),
),
{ initialValue: [] },
);
}
// toObservable emite ASINCRON (printr-un effect) și doar valoarea finală
// dacă semnalul e setat de mai multe ori în același tick.apasă butonulUnde RxJS rămâne mai bun
Un flux de mousemove cu throttle și acumulare (scan) e trivial în RxJS și incomod cu semnale. Rezultatul final îl pui într-un semnal pentru template.
0, 0 · evenimente procesate (throttle 40 ms): 0takeUntilDestroyed(): adio Subject destroy$
Orice subscribe() manual într-o componentă trebuie oprit la distrugere. takeUntilDestroyed() face asta automat folosind DestroyRef. (Cu toSignal nu ai nevoie de el — se dezabonează singur.)
import { DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export class Notifications {
private readonly destroyRef = inject(DestroyRef);
constructor() {
// ✓ în context de injecție: fără argument
this.socket.messages$.pipe(takeUntilDestroyed()).subscribe((m) => this.show(m));
}
connectLater() {
// ✓ în afara constructorului: dai explicit DestroyRef
this.poll$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe();
}
}
// Înainte: Subject + ngOnDestroy
// private destroy$ = new Subject<void>();
// ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }
// obs$.pipe(takeUntil(this.destroy$))- Creează un copil, apoi distruge-l și urmărește log-ul.
Outputs din și spre Observable
import { outputFromObservable, outputToObservable } from '@angular/core/rxjs-interop';
export class Search {
private readonly query$ = new Subject<string>();
// Un output construit dintr-un Observable
readonly queryChange = outputFromObservable(this.query$.pipe(debounceTime(300)));
}
// Invers: tratezi output-ul unei componente ca Observable (ex. într-o directivă)
const changes$ = outputToObservable(childComponentRef.instance.queryChange);Recomandare de arhitectură
// Recomandarea de arhitectură:
// RxJS la margini (I/O, evenimente), Signals pentru starea afișată.
@Service()
export class ChatStore {
private readonly socket = inject(ChatSocket); // expune Observable<Message>
// stream de evenimente → acumulat cu RxJS → expus ca semnal
readonly messages = toSignal(
this.socket.messages$.pipe(scan((all, m) => [...all, m].slice(-100), [] as Message[])),
{ initialValue: [] },
);
readonly unread = computed(() => this.messages().filter((m) => !m.read).length);
}Când DA, când NU
- Ai evenimente în timp: websocket, SSE, drag & drop, scroll infinit.
- Ai nevoie de operatori de timp/concurență:
switchMap,exhaustMap,retrycu backoff,bufferTime. - Combini multe surse asincrone cu reguli complexe.
- Lucrezi cu API-uri care expun Observable (Router events, Reactive Forms
valueChanges).
- Ții stare de UI (filtre, selecție, toggle-uri).
- Derivezi valori pentru template —
computede mai simplu decâtcombineLatest. - Faci GET-uri dependente de stare —
httpResource. - Folosești
BehaviorSubjectdoar ca „variabilă observabilă” — unsignale exact asta.
Capcane frecvente
toSignalîn afara contextului de injecție → eroare. Creează-l ca field sau dă-i{ injector }.toSignalpe un Observable „rece” HTTP în getter → un request nou la fiecare apel. Creează-l o singură dată.- Erori: dacă Observable-ul dă eroare, citirea semnalului aruncă. Prinde eroarea cu
catchErrorînainte. toObservablenu emite sincron: nu te baza că primești valoarea imediat dupăset().
Fișă rapidă
toSignal(obs$, { initialValue }) // Observable → Signal (auto-unsubscribe)
toSignal(obs$, { requireSync: true }) // fără undefined, dacă emite sincron
toObservable(sig) // Signal → Observable (emite asincron)
takeUntilDestroyed() // în context de injecție
takeUntilDestroyed(destroyRef) // oriunde
outputFromObservable(obs$) // Observable → output()
outputToObservable(output) // output() → Observable
rxResource({ params, stream }) // date async pe Observable → Resource
pendingUntilEvent() // ține aplicația „instabilă” (SSR) până la prima emisie