RxJS Interop: Uniendo lo mejor de dos mundos en Angular
Aunque Angular Signals es fantástico para el estado síncrono, RxJS sigue siendo el rey para manejar eventos asíncronos complejos (como debounce, switchMap o websockets). La clave del éxito en Angular moderno es saber cómo hacerlos trabajar juntos.
Angular proporciona el paquete @angular/core/rxjs-interop para facilitar esta comunicación.
De Observable a Signal: toSignal
El uso más común es consumir un flujo de datos (como una petición HTTP) en la vista sin usar el AsyncPipe. toSignal convierte un Observable en un Signal de lectura.
import { toSignal } from '@angular/core/rxjs-interop';
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({...})
export class UserListComponent {
private http = inject(HttpClient);
// El observable se suscribe automáticamente
users$ = this.http.get<User[]>('/api/users');
// Convertimos a Signal. Podemos definir un valor inicial.
users = toSignal(this.users$, { initialValue: [] });
}
En la plantilla, simplemente llamamos a users() y Angular se encarga de la reactividad fina.
¿Qué pasa con la suscripción?
toSignal se suscribe automáticamente al Observable y se desuscribe cuando el componente se destruye. No necesitas takeUntilDestroyed() ni ngOnDestroy.
// ❌ Antes: gestión manual
export class OldComponent implements OnDestroy {
private destroy$ = new Subject<void>();
users: User[] = [];
ngOnInit() {
this.http.get<User[]>('/api/users')
.pipe(takeUntil(this.destroy$))
.subscribe(users => this.users = users);
}
ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }
}
// ✅ Ahora: una sola línea
export class ModernComponent {
users = toSignal(inject(HttpClient).get<User[]>('/api/users'), { initialValue: [] });
}
De Signal a Observable: toObservable
A veces necesitamos reaccionar a cambios en un Signal usando operadores de RxJS (por ejemplo, para hacer un debounce en un input de búsqueda).
import { signal } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged, switchMap, catchError } from 'rxjs/operators';
import { of } from 'rxjs';
export class SearchComponent {
private searchService = inject(SearchService);
query = signal('');
// Convertimos el signal a observable para usar pipes de RxJS
private results$ = toObservable(this.query).pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term =>
term.length < 2
? of([])
: this.searchService.search(term).pipe(
catchError(() => of([]))
)
)
);
// Y lo convertimos de vuelta a Signal para la plantilla
results = toSignal(this.results$, { initialValue: [] });
updateQuery(e: Event) {
this.query.set((e.target as HTMLInputElement).value);
}
}
Observa el flujo circular: Signal → Observable → operadores RxJS → Signal. Esto nos da lo mejor de ambos mundos.
takeUntilDestroyed: El destructor automático
Otro helper esencial del paquete es takeUntilDestroyed(), que reemplaza el patrón subject + takeUntil:
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export class NotificationComponent {
private destroyRef = inject(DestroyRef);
ngOnInit() {
interval(5000).pipe(
takeUntilDestroyed(this.destroyRef),
switchMap(() => this.notificationService.check())
).subscribe(notifications => {
// Se cancela automáticamente al destruir el componente
});
}
}
El poder de effect() vs subscribe()
Mientras que en RxJS nos suscribimos manualmente, con Signals usamos effect(). Un efecto se ejecuta siempre que uno de los signals que lee cambia.
- Usa
toSignalpara traer datos a la UI. - Usa
toObservablecuando necesites manipular el tiempo o flujos complejos. - Usa
takeUntilDestroyedcuando trabajes con suscripciones imperativas.
Esta interoperabilidad nos permite eliminar gran parte de la complejidad de gestión de suscripciones manuales (ngOnDestroy o takeUntil).