Icono del sitio Profile Software Services

Cómo usar Observables en Angular paso a paso

Observables en Angular

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

Piezas básicas del modelo:

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:

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

Operadores de tiempo

Operadores de concurrencia

Estos se usan dentro de pipe, casi siempre combinados con Observables que emiten “triggers” (inputs, clicks, paramMap…):

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:

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:

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.)?

En la práctica, hoy en Angular:

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:

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 blogredes sociales y síguenos en nuestro Canal de YouTube donde encontrarás un montón de contenidos interesantes.

Salir de la versión móvil