Cómo combinar Async Pipes en Angular: De combineLatest a Signals y @let

⚡ ¿Usas Angular Moderno (v17+)? Salta directamente a 2. El enfoque moderno: Convertir flujos con toSignal() para eliminar el pipe
asyncpor completo.
Si estás construyendo una aplicación en Angular que obtiene datos de varias APIs, es muy probable que hayas escrito una plantilla como esta:
<!-- ⚠️ El problema: Múltiples async pipes que provocan peticiones duplicadas -->
<div *ngIf="user$ | async as user">
<h2>{{ (user$ | async)?.name }}</h2>
<div *ngIf="stats$ | async as stats">
<p>Points: {{ (stats$ | async)?.points }}</p>
</div>
</div>Este es un error común y costoso. Si user$ representa una petición HTTP, usar | async varias veces dispara múltiples peticiones de red idénticas. Para empeorar las cosas, anidar directivas estructurales *ngIf genera un HTML profundo y difícil de leer.
Aquí tienes un resumen rápido de cómo solucionaremos este problema usando patrones modernos de Angular:
| Patrón | Ideal para | ¿Requiere Async Pipe? | Peticiones de red |
|---|---|---|---|
combineLatest (ViewModel) | Pipelines de datos complejos en RxJS | Sí (Solo 1 en la raíz) | 1 |
Signals (toSignal) | Aplicaciones modernas de Angular Zoneless | No (Lectura directa del valor) | 1 |
Sintaxis @let (Angular 18+) | Alias rápidos de variables en la plantilla | Sí | 1 |
Vamos a revisar las tres formas más limpias de combinar flujos y eliminar suscripciones duplicadas.
1. El patrón clásico de RxJS: ViewModel con combineLatest 🔄
Si tu proyecto depende en gran medida de flujos con RxJS, el patrón más sólido es combinar todos los observables independientes en un único flujo vm$ (ViewModel) dentro de la clase de tu componente:
import { Component, inject } from '@angular/core';
import { combineLatest, Observable } from 'rxjs';
import { PlayerService, Player, PlayerStats } from './player.service';
interface PlayerViewModel {
player: Player;
stats: PlayerStats;
}
@Component({
selector: 'app-player-profile',
templateUrl: './player-profile.component.html'
})
export class PlayerProfileComponent {
private playerService = inject(PlayerService);
private playerId = 23;
player$: Observable<Player> = this.playerService.getPlayer(this.playerId);
stats$: Observable<PlayerStats> = this.playerService.getStats(this.playerId);
// Combina múltiples flujos en un único observable ViewModel
vm$: Observable<PlayerViewModel> = combineLatest({
player: this.player$,
stats: this.stats$
});
}En la plantilla (Flujo de control moderno)
Con el bloque @if del Angular moderno, solo necesitas un único pipe async en la raíz de la vista. Una vez desenvuelto, accedes a todas las propiedades de manera síncrona:
@if (vm$ | async; as vm) {
<div class="player-card">
<h2>{{ vm.player.name }}</h2>
<p>Position: {{ vm.player.position }}</p>
<ul>
<li>Points: {{ vm.stats.points }}</li>
<li>Rebounds: {{ vm.stats.rebounds }}</li>
</ul>
</div>
} @else {
<div class="skeleton-loader">Loading player profile...</div>
}Ahora tenemos exactamente una sola suscripción y cero peticiones de red duplicadas.
A continuación, veamos cómo los Signals en Angular moderno hacen este proceso todavía más simple.
2. El enfoque moderno: Convertir flujos con toSignal() ⚡
Con los Signals de Angular, puedes eliminar el pipe async por completo. El paquete @angular/core/rxjs-interop ofrece la función toSignal(), que se suscribe al Observable internamente y lo expone como un Signal reactivo y síncrono:
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { PlayerService } from './player.service';
@Component({
selector: 'app-player-signals',
standalone: true,
template: `
@if (player(); as p) {
<div class="player-card">
<h2>{{ p.name }}</h2>
@if (stats(); as s) {
<p>Points: {{ s.points }} | Rebounds: {{ s.rebounds }}</p>
}
</div>
} @else {
<p>Loading player...</p>
}
`
})
export class PlayerSignalsComponent {
private playerService = inject(PlayerService);
private playerId = 23;
// Gestiona automáticamente el ciclo de vida de suscripción y cancelación
player = toSignal(this.playerService.getPlayer(this.playerId));
stats = toSignal(this.playerService.getStats(this.playerId));
}Por qué toSignal() es el estándar recomendado:
- Sin cancelaciones manuales de suscripción: Las suscripciones están vinculadas directamente al ciclo de vida del componente y se limpian automáticamente.
- Lectura síncrona en la plantilla: Lees los valores con llamadas a funciones normales (
player()). No necesitas ningún pipeasync. - Reactividad granular: Las actualizaciones solo disparan las vinculaciones del DOM necesarias, preparando tu aplicación para la detección de cambios Zoneless.
Ahora revisemos la incorporación más reciente en Angular 18.1: la declaración @let.
3. La solución en Angular 18.1+: Variables de plantilla con @let 🎯
A partir de Angular 18.1, puedes declarar variables locales directamente dentro de la plantilla usando @let. Esto elimina la necesidad de crear elementos contenedores ficticios solo para asignarle un alias al pipe async:
@let player = player$ | async;
@let stats = stats$ | async;
@if (player && stats) {
<div class="player-container">
<h2>{{ player.name }}</h2>
<p>Points: {{ stats.points }}</p>
<p>Assists: {{ stats.assists }}</p>
</div>
}Beneficios clave de @let:
- Funciona en cualquier parte del ámbito de la plantilla.
- Ofrece inferencia de tipos estricta para las expresiones secundarias.
- Evita crear contenedores
ng-containeradicionales en el DOM solo para desenvolver un observable.
Resumen
Para mantener tus aplicaciones de Angular rápidas y limpias:
- Nunca repitas pipes
asyncsobre el mismo observable en diferentes elementos de la plantilla. Provoca peticiones de red duplicadas. - Usa
combineLatestpara construir un ViewModelvm$unificado cuando trabajes con flujos complejos de RxJS. - Adopta
toSignal()en componentes modernos para beneficiarte de la lectura síncrona en plantillas y de la reactividad de Signals. - Usa
@leten Angular 18+ para simplificar el alcance de variables en la plantilla sin anidar elementosng-container.
Si quieres conocer más técnicas de arquitectura en Angular moderno, revisa mis guías sobre Preguntas esenciales para entrevistas de Angular y Cómo compartir datos entre componentes en Angular.
These are my experiences learning and facing my daily challenges working with Angular.
Preguntas Frecuentes
¿Por qué es malo usar múltiples async pipes para el mismo observable en Angular?
Cada async pipe aplicado a un observable crea una suscripción independiente. Si el observable es una petición HTTP o un cálculo sin caché, múltiples async pipes provocan peticiones de red duplicadas y ciclos innecesarios de detección de cambios.
¿Cómo reemplazan los Signals de Angular (toSignal) al async pipe?
La función toSignal() de @angular/core/rxjs-interop convierte un observable de RxJS en un Signal. En la plantilla, lees el valor de forma síncrona con una llamada a función data(), eliminando por completo el async pipe.
¿Qué es la sintaxis @let en Angular 18.1+?
La sintaxis @let te permite declarar variables locales en la plantilla (@let user = user$ | async) sin envolver tu HTML en elementos contenedores innecesarios o directivas estructurales vacías.
¿Cómo se combinan múltiples observables en un solo async pipe en Angular clásico?
Puedes usar el operador combineLatest de RxJS en la clase del componente para agrupar múltiples flujos en un único observable vm$ (ViewModel), desenvolviéndolo una sola vez en la plantilla con *ngIf='vm$ | async as vm'.
Artículos Relacionados
Preguntas esenciales de entrevista en Angular para desarrolladores Junior y Mid-Level
Una guía práctica de preguntas de entrevista sobre Angular moderno para roles junior y mid-level, cubriendo Signals, Standalone APIs, Control Flow, Vitest y Zoneless Angular.
Entendiendo composición vs. herencia en Angular
Por qué la herencia de clases suele generar 'constructor hell' en Angular, y cómo usar la Directive Composition API (hostDirectives), inject() y Signals para escribir código limpio.
Cómo manejar y capturar errores en RxJS y Angular
Aprende a capturar, manejar y recuperarte de errores en flujos de RxJS y Angular usando catchError, throwError, EMPTY y la interoperabilidad con Signals en Angular moderno.
Cómo compartir datos entre componentes en Angular (Signals y servicios)
Aprende los mejores patrones para compartir datos entre componentes en Angular. Compara los decoradores clásicos Input/Output y BehaviorSubjects con las Signals modernas: input(), output(), model() y servicios basados en Signals.
Compartir este artículo
Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.
Software real. Lecciones reales.
Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.
Únete a 13,800+ desarrolladores y lectores.
Cero spam. Cancela tu suscripción en cualquier momento.