Volver al blog
Angular RxJS Signals Async
Imagen de portada del artículo: RxJS Interop: Uniendo lo mejor de dos mundos en Angular

RxJS Interop: Uniendo lo mejor de dos mundos en Angular

13 min
German

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.

typescript
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.

typescript
// ❌ 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).

typescript
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:

typescript
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.

Esta interoperabilidad nos permite eliminar gran parte de la complejidad de gestión de suscripciones manuales (ngOnDestroy o takeUntil).