A medida que las aplicaciones front crecen en complejidad, la gestión de asincronía, eventos y flujos de datos se vuelve tan importante como el propio renderizado de la UI. En Angular, ese problema no lo resuelve el framework “a pelo”, sino RxJS: Observables, operadores y patrones de composición.
Los Observables en Angular son el modelo de reactividad “de streaming” de Angular. Permiten describir qué quieres que pase con un flujo de datos (HTTP, inputs, websockets, timers…) sin caer en cascadas de callbacks o lógica imperativa dispersa. Compones con operadores, te suscribes donde tiene sentido y controlas el ciclo de vida.
En este artículo veremos:
- Qué son los Observables en Angular y cómo pensarlos.
- Patrón mental: fuente → pipe(operadores) → subscribe / async.
- Uso típico con HttpClient, Router, formularios reactivos y estado compartido.
- Patrones con switchMap, mergeMap, etc., y cómo elegir el adecuado.
- Mejores prácticas y cuándo tiene sentido seguir con Observables frente a otras alternativas (Signals, Promises, etc.).
¿Qué es un Observable en Angular?
Un Observable es una fuente de datos que puede emitir 0..N valores a lo largo del tiempo. No hace nada hasta que alguien se suscribe. Esa pereza (lazy) es clave: te permite describir un flujo sin ejecutarlo hasta que la UI o el código de negocio lo necesita.
En Angular te los encuentras por todas partes:
HttpClient.get<T>() → Observable<T>ActivatedRoute.paramMap,queryParamMap, eventsFormControl.valueChanges,FormGroup.valueChanges- Streams creados desde eventos del DOM (fromEvent)
- Servicios que exponen estado con Subject /
BehaviorSubject
Piezas básicas del modelo:
- Observable<T>: describe un flujo.
- Observer: recibe next, error, complete.
- Subscription: el contrato que puedes cancelar (
unsubscribe()). - Operadores: funciones puras que transforman/combinan flujos vía pipe.
Un ejemplo muy simple:
import { of } from 'rxjs'; // 'of' nos permite crear un Observable a partir de una lista de valores síncronos.
import { map, filter } from 'rxjs/operators'; // 'map' y 'filter' son operadores funcionales que transforman y filtran las emisiones.
// Este Observable es "frío" y "perezoso": no hace absolutamente nada hasta que alguien se suscribe.
const numbers$ = of(1, 2, 3, 4, 5);
// 'pipe' es la forma idiomática en RxJS de componer transformaciones puras sobre un flujo.
const evensDoubled$ = numbers$.pipe(
// 'filter' deja pasar solo los valores que cumplan la condición.
filter(n => n % 2 === 0),
// 'map' transforma cada valor que pasa por el flujo.
map(n => n * 2)
);
// Al suscribirse el Observable empieza a emitir sus valores correspondientes.
evensDoubled$.subscribe(value => {
console.log(value); // Output: 4, 8
});
Patrón mental que vas a repetir una y otra vez:
fuente$ → pipe(operadores) → subscribe()
En plantilla: fuente$ → pipe(operadores) → | async
Observables en Angular: las fuentes más comunes
1. HttpClient
La API HTTP moderna de Angular ya está totalmente basada en Observables:
@Injectable({ providedIn: 'root' })
export class UsersApi {
constructor(private readonly http: HttpClient) {}
// Se retornara un observable listo para suscribirse
getUsers() {
return this.http.get<User[]>('/api/users');
}
getUser(id: string) {
return this.http.get<User>(/api/users/${id});
}
}
No devuelve Promises, sino Observable<T>. Eso te permite:
- Componer con otros streams (router, formularios, etc.).
- Cancelar peticiones (switchMap, takeUntil, unsubscribe).
- Encadenar transformaciones (map, tap, catchError…).
2. Router (paramMap, query params, navegación)
El router expone su estado como Observables:
@Component({
selector: 'app-user-detail',
standalone: true,
template:
@if (user$ | async; as user) {
<h1>{{ user.name }}</h1>
} @else {
<p>Cargando...</p>
}
})
export class UserDetailComponent {
readonly user$ = this.route.paramMap.pipe(
map(params => params.get('id')),
filter((id): id is string => !!id),
switchMap(id => this.usersApi.getUser(id))
);
constructor(
private readonly route: ActivatedRoute,
private readonly usersApi: UsersApi
) {}
}
Cada vez que cambia el id en la URL, el stream reacciona y dispara una nueva petición HTTP.
3. Formularios reactivos (valueChanges)
Con formularios reactivos, valueChanges y Observables encajan perfecto:
@Component({
selector: 'app-user-search',
standalone: true,
imports: [ReactiveFormsModule, NgFor],
template:
<input type="text" [formControl]="searchControl" placeholder="Buscar usuario..." />
<ul>
<li *ngFor="let user of results$ | async">
{{ user.name }}
</li>
</ul>
})
export class UserSearchComponent {
readonly searchControl = new FormControl('', { nonNullable: true });
readonly results$ = this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(query => this.usersApi.searchUsers(query)),
catchError(() => of([])) // fallback en caso de error
);
constructor(private readonly usersApi: UsersApi) {}
}
Este patrón (valueChanges + debounce + distinct + switchMap) es prácticamente un staple en apps reales.
Operadores clave y cómo elegirlos
La mayoría de “magia” con Observables en Angular viene de elegir bien operadores, sobre todo los de concurrencia:
Operadores de transformación comunes
map:transforma valores.filter:filtra emisiones.tap:efectos secundarios (logs, métricas).scan:reduce sobre el tiempo (tipo reduce pero continuo).
Operadores de tiempo
debounceTime:espera un silencio antes de emitir.throttleTime:limita frecuencia de emisiones.delay:retrasa emisiones.
Operadores de concurrencia
Estos se usan dentro de pipe, casi siempre combinados con Observables que emiten “triggers” (inputs, clicks, paramMap…):
- switchMap
- Cancela la petición anterior si llega una nueva.
- Ideal para búsquedas, autocompletados, dependencias encadenadas.
- concatMap
- Cola las ejecuciones; se procesa una detrás de otra.
- Útil cuando el orden importa (p. ej. operaciones secuenciales).
- mergeMap
- Todo en paralelo, sin cancelar nada.
- Bien cuando las respuestas no dependen unas de otras y el orden da igual.
- exhaustMap
- Ignora triggers nuevos mientras haya uno en curso.
- Perfecto para evitar dobles envíos de formularios o spameo de botones.
Ejemplo típico de botón de login con exhaustMap:
readonly loginClick$ = fromEvent(this.loginButton.nativeElement, 'click');
readonly login$ = this.loginClick$.pipe(
exhaustMap(() =>
this.authApi.login(this.form.value).pipe(
tap(() => this.router.navigate(['/dashboard'])),
catchError(err => {
this.error.set('Credenciales inválidas');
return EMPTY;
})
)
)
);
Aquí, aunque el usuario haga 50 clicks, solo se procesa uno a la vez.
Estado compartido ligero con BehaviorSubject
Para estado compartido simple no necesitas NgRx ni librerías pesadas. Un servicio con BehaviorSubject y Observables suele ser suficiente.
interface UserState {
users: User[];
loading: boolean;
}
@Injectable({ providedIn: 'root' })
export class UsersStore {
private readonly _state = new BehaviorSubject<UserState>({
users: [],
loading: false
});
readonly state$ = this._state.asObservable();
readonly users$ = this.state$.pipe(pluck('users'));
readonly loading$ = this.state$.pipe(pluck('loading'));
constructor(private readonly api: UsersApi) {}
loadUsers() {
this.patchState({ loading: true });
this.api.getUsers().pipe(
tap(users => this.patchState({ users, loading: false })),
catchError(err => {
console.error(err);
this.patchState({ loading: false });
return EMPTY;
})
).subscribe();
}
private patchState(partial: Partial<UserState>) {
this._state.next({
...this._state.value,
...partial,
});
}
}
@Component({
selector: 'app-users-page',
standalone: true,
template:
@if (store.loading$ | async) {
<p>Cargando...</p>
}
<ul>
@for (user of store.users$ | async; track user.id) {
<li>{{ user.name }}</li>
}
</ul>
})
export class UsersPageComponent {
constructor(readonly store: UsersStore) {
this.store.loadUsers();
}
}
Esto ya es un mini store razonable para muchas pantallas sin meter complejidad extra.
Ciclo de vida: async pipe vs subscribe manual
Regla general de supervivencia: «Si puedes usar async pipe, úsalo. Si tienes que usar subscribe, limpia tú».
Opción recomendada: async pipe
Suscribe y desuscribe solo:
<div *ngIf="user$ | async as user">
{{ user.name }}
</div>
Cuando necesitas «subscribe manual«
Casos típicos:
- Lanzar navegación (router.navigate) desde dentro de una respuesta.
- Integrar con APIs no reactivas.
- Actualizar algo que no está directamente en plantilla (ej. signal, estado local, etc.).
En esos casos:
private readonly destroy$ = new Subject<void>();
ngOnInit() {
this.usersStore.users$
.pipe(takeUntil(this.destroy$))
.subscribe(users => {
console.log('Users length:', users.length);
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
O si usas Subscription:
private sub = new Subscription();
ngOnInit() {
this.sub.add(
this.usersStore.users$.subscribe(/* ... */)
);
}
ngOnDestroy() {
this.sub.unsubscribe();
}
Buenas prácticas de Observables en Angular
Evita suscripciones anidadas
En lugar de:
this.api.getUser(id).subscribe(user => {
this.api.getOrders(user.id).subscribe(orders => {
// ...
});
});
usa composición:
this.api.getUser(id).pipe(
switchMap(user => this.api.getOrders(user.id)),
).subscribe(orders => { /* ... */ });
Tipa siempre tus Observables
Observable<User[]>, mejor que Observable<any>.
Gestiona errores dentro del flujo
No dejes que un error tumbe tu stream y tu UI:
this.data$ = this.api.getData().pipe(
catchError(err => {
this.errorMsg.set('Error al cargar datos');
return of([]); // algo seguro
})
);
Evita duplicar estado
Si puedes derivarlo en el stream, hazlo ahí y no lo dupliques en propiedades sueltas.
Usa el operador correcto de concurrencia
Según el caso:
- switchMap: cancela lo anterior → búsquedas, router-based data.
- concatMap: orden importa → colas, steps.
- mergeMap: paralelismo → cuando el orden no es relevante.
- exhaustMap: anti-spam → botones, forms, procesos largos.
Comparte resultados cuando tenga sentido
Si un stream HTTP se usa en varios sitios, evita disparar la petición varias veces:
readonly users$ = this.api.getUsers().pipe(
shareReplay({ bufferSize: 1, refCount: true })
);
¿Observables en Angular vs otras opciones (Signals, Promises, etc.)?
- Observables
- Flujos asíncronos, múltiples valores, cancelación, composición avanzada.
- Perfectos para HTTP, router, formularios, websockets, polling, etc.
- Signals (Angular 16+)
- Estado de presentación, síncrono, granularidad de re-render.
- Geniales para UI local, flags, derivados, etc.
- Promesas
- Bien para un único valor sencillo, pero se quedan cortas en temas de cancelación y composición compleja.
En la práctica, hoy en Angular:
- Usa Observables para todo lo que sea I/O, streaming, orquestación.
- Usa Signals para estado de UI y derivaciones locales.
- Combina ambos con toSignal() / toObservable() cuando haga falta.
Conclusión
Observables no son “una cosa más de Angular”: son la columna vertebral de la asincronía en el framework. Bien usados, te permiten:
- Encadenar procesos complejos sin convertir tu código en una marabunta de callbacks.
- Cancelar lo que ya no tiene sentido (peticiones, búsquedas, acciones duplicadas).
- Integrar router, formularios y HTTP bajo una misma mentalidad declarativa.
Dominar el patrón fuente → pipe(operadores) → subscribe / async, entender bien los operadores de concurrencia y saber dónde cortar el ciclo de vida marcan bastante la diferencia entre un código que se mantiene solo… y otro que nadie quiere tocar.
¿Quieres aprender más sobre Observables en Angular? No te pierdas nuestro blog, redes sociales y síguenos en nuestro Canal de YouTube donde encontrarás un montón de contenidos interesantes.
