Skip to content

Cold Observable y Hot Observable

Uno de los conceptos clave al usar RxJS es la distinción entre "Cold Observable" y "Hot Observable". Entender esta distinción es esencial para aprender cómo usar Observable de manera eficiente.

Por Qué es Importante Entender Cold/Hot

Si no entiende la distinción Cold/Hot, enfrentará los siguientes problemas:

  • Ejecución Duplicada No Intencionada - Las llamadas de API se ejecutan múltiples veces
  • Fugas de memoria - Las suscripciones no se gestionan apropiadamente
  • Problemas de rendimiento - El procesamiento innecesario se repite
  • Inconsistencias de datos - No se reciben los datos esperados

Diferencias Cold vs Hot (Gráfico Comparativo)

Primero, obtengamos la visión general.

Ítem de ComparaciónCold ObservableHot Observable
Ejecución sin suscripciónNo se ejecuta (se ejecuta solo al suscribirse)Se ejecuta (fluye valores incluso sin subscribe)
Momento de publicación de datosComienza cuando se llama a subscribe()Comienza en el momento del publicador (independiente de la suscripción)
Reutilización de ejecuciónSe ejecuta de nuevo cada vezComparte el flujo existente con múltiples suscriptores
Consistencia de datosCada suscripción recibe valores independientesNo puede recibir valores pasados si se suscribe a mitad de flujo
Principales casos de usoSolicitudes HTTP, procesamiento asíncronoEventos de UI, WebSocket, comunicación en tiempo real
Escenarios de usoCuando cada proceso es independienteCompartir estado, difusión de eventos

Criterios: ¿Debería el procesamiento ejecutarse nuevamente para cada suscriptor? ¿O debería compartirse el flujo?

Criterios de Decisión Cold vs. Hot

Los siguientes criterios se pueden usar para determinar si un Observable es realmente Cold o Hot:

Punto de DecisiónColdHot
¿Se vuelve a ejecutar la lógica de ejecución para cada suscripción?✅ Se vuelve a ejecutar cada vez❌ Compartir ejecución
¿Fluyen los datos antes de la suscripción?❌ Espera hasta suscribirse✅ Fluye independientemente de la suscripción
¿Múltiples suscripciones reciben los mismos datos?❌ Datos independientes✅ Compartir los mismos datos

Formas Prácticas de Distinguir

La siguiente prueba puede determinar fácilmente:

typescript
const observable$ = /* Observable a examinar */;

observable$.subscribe(/* suscripción 1 */);
observable$.subscribe(/* suscripción 2 */);

// ✅ Cold: console.log dentro de Observable se ejecuta dos veces
//         (la lógica de ejecución se vuelve a ejecutar para cada suscripción)
// ✅ Hot:  console.log dentro de Observable se ejecuta solo una vez
//         (la ejecución se comparte)

Ejemplo Concreto:

typescript
import { Observable, Subject } from 'rxjs';

// Cold Observable
const cold$ = new Observable(subscriber => {
  console.log('Cold: Ejecución iniciada');
  subscriber.next(Math.random());
});

cold$.subscribe(v => console.log('Suscripción 1:', v));
cold$.subscribe(v => console.log('Suscripción 2:', v));
// Salida:
// Cold: Ejecución iniciada  ← 1ra vez
// Suscripción 1: 0.123...
// Cold: Ejecución iniciada  ← 2da vez (re-ejecutado)
// Suscripción 2: 0.456...

// Hot Observable
const hot$ = new Subject();

hot$.subscribe(v => console.log('Suscripción 1:', v));
hot$.subscribe(v => console.log('Suscripción 2:', v));
hot$.next(1); // Datos publicados solo una vez
// Salida:
// Suscripción 1: 1
// Suscripción 2: 1  ← Comparte los mismos datos

Tabla de Clasificación Cold/Hot por Creation Function

Esta tabla clasifica Cold/Hot para todas las Creation Functions principales. Esto le permite ver de un vistazo qué función produce qué Observable.

CategoríaCreation FunctionCold/HotNotas
Creación básicaof()❄️ ColdRe-publica valores para cada suscripción
from()❄️ ColdRe-ejecuta array/Promise para cada suscripción
fromEvent()❄️ ColdAgrega escuchador independiente para cada suscripción [1]
interval()❄️ ColdTemporizador independiente para cada suscripción
timer()❄️ ColdTemporizador independiente para cada suscripción
Generación de buclerange()❄️ ColdRegenera rango para cada suscripción
generate()❄️ ColdRe-ejecuta bucle para cada suscripción
Comunicación HTTPajax()❄️ ColdNueva solicitud HTTP para cada suscripción
fromFetch()❄️ ColdNueva solicitud Fetch para cada suscripción
Combinaciónconcat()❄️ ColdHereda naturaleza de Observables fuente [2]
merge()❄️ ColdHereda naturaleza de Observables fuente [2:1]
combineLatest()❄️ ColdHereda naturaleza de Observables fuente [2:2]
zip()❄️ ColdHereda naturaleza de Observables fuente [2:3]
forkJoin()❄️ ColdHereda naturaleza de Observables fuente [2:4]
Selección/Particiónrace()❄️ ColdHereda naturaleza de Observables fuente [2:5]
partition()❄️ ColdHereda naturaleza de Observables fuente [2:6]
Condicionaliif()❄️ ColdHereda naturaleza de Observable condicionalmente seleccionado
defer()❄️ ColdEjecuta función factory para cada suscripción
Controlscheduled()❄️ ColdHereda naturaleza de Observable fuente
using()❄️ ColdCrea recurso para cada suscripción
Familia Subjectnew Subject()🔥 HotSiempre Hot
new BehaviorSubject()🔥 HotSiempre Hot
new ReplaySubject()🔥 HotSiempre Hot
new AsyncSubject()🔥 HotSiempre Hot
WebSocketwebSocket()🔥 HotComparte conexión WebSocket

Principio Clave

Casi todas las Creation Functions generan Cold. Solo las siguientes generan Hot:

  • Familia Subject (Subject, BehaviorSubject, ReplaySubject, AsyncSubject)
  • webSocket()

Cold Observable

Características

  • Se crea un nuevo flujo de datos cada vez que se realiza una suscripción
  • La publicación de datos no comienza hasta que se suscribe (ejecución perezosa)
  • Todos los suscriptores reciben todos los datos desde el principio del Observable

Cold Observable crea un nuevo contexto de ejecución cada vez que se suscribe. Esto es apropiado para solicitudes HTTP, procesamiento asíncrono y otras situaciones donde se requiere nuevo procesamiento cada vez.

Ejemplo de Código

typescript
import { Observable } from 'rxjs';

// Ejemplo de Cold Observable
const cold$ = new Observable<number>(subscriber => {
  console.log('Creación de fuente de datos - nueva suscripción');
  const randomValue = Math.random();
  subscriber.next(randomValue);
  subscriber.complete();
});

// Primera suscripción
console.log('--- Primera suscripción ---');
cold$.subscribe(value => console.log('Suscriptor 1:', value));

// Segunda suscripción (se generan datos diferentes)
console.log('--- Segunda suscripción ---');
cold$.subscribe(value => console.log('Suscriptor 2:', value));

Salida

sh
--- Primera suscripción ---
Creación de fuente de datos - nueva suscripción
Suscriptor 1: 0.259632...
--- Segunda suscripción ---
Creación de fuente de datos - nueva suscripción Re-ejecutado
Suscriptor 2: 0.744322... Valor diferente

Punto Importante

Cada suscripción ejecuta "Creación de fuente de datos" y genera valores diferentes.

Observables Cold Comunes (Cómo Identificar)

Los siguientes Observables son usualmente Cold:

typescript
import { of, from, interval, timer } from 'rxjs';
import { ajax } from 'rxjs/ajax';

// Creation Functions
of(1, 2, 3)                    // Cold
from([1, 2, 3])                // Cold
from(fetch('/api/data'))       // Cold

// Operadores de tiempo
interval(1000)                 // Cold
timer(1000)                    // Cold

// Solicitudes HTTP
ajax('/api/users')             // Cold

Regla

Creation Functions, operadores de tiempo y solicitudes HTTP son básicamente Cold

Hot Observable

Características

  • Fluye valores incluso si no está suscrito (se ejecuta con o sin suscripción)
  • Solo recibe datos desde el punto de inicio de suscripción en adelante
  • Una fuente de datos es compartida por múltiples suscriptores

Con Hot Observable, el momento de publicación de flujo es independiente de la suscripción, y los suscriptores se unen a mitad del flujo.

Ejemplo de Código

typescript
import { Subject } from 'rxjs';

// Ejemplo de Hot Observable (usando Subject)
const hot$ = new Subject<number>();

// Primera suscripción
console.log('--- Suscriptor 1 comienza ---');
hot$.subscribe(value => console.log('Suscriptor 1:', value));

// Publicar datos
hot$.next(1);
hot$.next(2);

// Segunda suscripción (suscripción tardía)
console.log('--- Suscriptor 2 comienza ---');
hot$.subscribe(value => console.log('Suscriptor 2:', value));

// Publicar más datos
hot$.next(3);
hot$.next(4);

hot$.complete();

Salida

sh
--- Suscriptor 1 comienza ---
Suscriptor 1: 1
Suscriptor 1: 2
--- Suscriptor 2 comienza ---
Suscriptor 1: 3
Suscriptor 2: 3 Suscripción 2 se une desde 3 (no puede recibir 1, 2)
Suscriptor 1: 4
Suscriptor 2: 4

Punto Importante

Suscriptor 2 se unió a mitad de flujo y no puede recibir valores pasados (1, 2).

Observables Hot Comunes (Cómo Identificar)

Los siguientes Observables son siempre Hot:

typescript
import { Subject, BehaviorSubject, ReplaySubject } from 'rxjs';
import { webSocket } from 'rxjs/webSocket';

// Familia Subject (siempre Hot)
new Subject()                  // Hot
new BehaviorSubject(0)         // Hot
new ReplaySubject(1)           // Hot

// WebSocket (siempre Hot)
webSocket('ws://localhost:8080') // Hot

Regla

Solo la familia Subject y webSocket() generan Hot

fromEvent() es Cold

fromEvent(button, 'click') a menudo se malinterpreta como Hot, pero en realidad es Cold. Agrega un escuchador de eventos independiente para cada suscripción. El evento en sí ocurre independientemente de la suscripción, pero cada suscriptor tiene un escuchador independiente.

Cómo Convertir Cold Observable a Hot

En RxJS, los siguientes son los medios principales para convertir Cold Observable a Hot:

  • share() - Conversión hot simple (recomendado)
  • shareReplay() - Almacenar en caché valores pasados y convertir a hot
  • multicast() - Obsoleto (obsoleto en RxJS v7, eliminado en v8)

Operador share()

share() es la forma más común de convertir un Cold Observable a un Hot Observable.

typescript
import { interval } from 'rxjs';
import { share, take } from 'rxjs';

// Simular llamada HTTP
const makeHttpRequest = () => {
  console.log('¡Llamada HTTP ejecutada!');
  return interval(1000).pipe(take(3));
};

// ❌ Cold Observable (sin compartir)
const cold$ = makeHttpRequest();

cold$.subscribe(val => console.log('Suscriptor 1:', val));
cold$.subscribe(val => console.log('Suscriptor 2:', val));
// → Llamada HTTP ejecutada dos veces

// ✅ Hot Observable (usando share)
const shared$ = makeHttpRequest().pipe(share());

shared$.subscribe(val => console.log('Suscriptor compartido 1:', val));
shared$.subscribe(val => console.log('Suscriptor compartido 2:', val));
// → Llamada HTTP ejecutada solo una vez, el resultado se comparte

Salida (Cold):

sh
¡Llamada HTTP ejecutada! 1ra vez
Suscriptor 1: 0
¡Llamada HTTP ejecutada! 2da vez (¡duplicado!)
Suscriptor 2: 0
...

Salida (Hot):

sh
¡Llamada HTTP ejecutada! Solo una vez
Suscriptor compartido 1: 0
Suscriptor compartido 2: 0 Comparte el mismo flujo
...

Casos de Uso

  • Usar los mismos resultados de API en múltiples componentes
  • Evitar efectos secundarios duplicados (ej., llamadas HTTP)

Operador shareReplay()

shareReplay() es una extensión de share() que almacena en caché valores pasados y los reproduce para nuevos suscriptores.

typescript
import { interval } from 'rxjs';
import { shareReplay, take } from 'rxjs';

const request$ = interval(1000).pipe(
  take(3),
  shareReplay(2)  // Almacenar en caché los últimos 2 valores
);

// Primera suscripción
request$.subscribe(val => console.log('Suscriptor 1:', val));

// Segunda suscripción después de 3.5 segundos (después de completar el flujo)
setTimeout(() => {
  console.log('--- Suscriptor 2 comienza (después de finalización) ---');
  request$.subscribe(val => console.log('Suscriptor 2:', val));
}, 3500);

Salida

sh
Suscriptor 1: 0
Suscriptor 1: 1
Suscriptor 1: 2
--- Suscriptor 2 comienza (después de finalización) ---
Suscriptor 2: 1 Valor en caché (últimos 2)
Suscriptor 2: 2 Valor en caché

Casos de Uso

  • Almacenar en caché resultados de API
  • Compartir estado inicial (almacenar en caché solo el último)
  • Proporcionar datos históricos a suscriptores tardíos

Notas sobre shareReplay

shareReplay() continúa manteniendo el caché incluso cuando las suscripciones llegan a 0, lo que puede causar fugas de memoria. Consulte Capítulo 10: Mal uso de shareReplay para más información.

Acerca de multicast()

NOTE

multicast() es flexible, pero fue obsoleto en RxJS v7 y eliminado en v8. Use share() o shareReplay() ahora. Consulte descripción del operador share() para más información.

Ejemplo Práctico: Servicio de Caché de API

Patrón común en aplicaciones reales: múltiples componentes necesitan los mismos datos de API.

typescript
import { Observable, of, throwError } from 'rxjs';
import { catchError, shareReplay, delay, tap } from 'rxjs';

// Servicio de caché simple
class UserService {
  private cache$: Observable<User[]> | null = null;

  getUsers(): Observable<User[]> {
    // Devolver caché si existe
    if (this.cache$) {
      console.log('Devolviendo desde caché');
      return this.cache$;
    }

    // Crear nueva solicitud y almacenar en caché
    console.log('Ejecutando nueva solicitud');
    this.cache$ = this.fetchUsersFromAPI().pipe(
      catchError(err => {
        this.cache$ = null;  // Limpiar caché en caso de error
        return throwError(() => err);
      }),
      shareReplay(1)  // Almacenar en caché el último resultado
    );

    return this.cache$;
  }

  private fetchUsersFromAPI(): Observable<User[]> {
    // Simular solicitud de API real
    return of([
      { id: 1, name: 'Taro Yamada' },
      { id: 2, name: 'Hanako Sato' }
    ]).pipe(
      delay(1000),
      tap(() => console.log('Datos recibidos de API'))
    );
  }

  clearCache(): void {
    this.cache$ = null;
    console.log('Caché limpiado');
  }
}

interface User {
  id: number;
  name: string;
}

// Ejemplo de uso
const userService = new UserService();

// Componente 1: Solicitar datos
userService.getUsers().subscribe(users =>
  console.log('Componente 1:', users)
);

// Componente 2: Solicitar datos después de 2 segundos
setTimeout(() => {
  userService.getUsers().subscribe(users =>
    console.log('Componente 2:', users)
  );
}, 2000);

// Limpiar caché y solicitar nuevamente
setTimeout(() => {
  userService.clearCache();
  userService.getUsers().subscribe(users =>
    console.log('Componente 3:', users)
  );
}, 4000);

Salida

sh
Ejecutando nueva solicitud
Datos recibidos de API
Componente 1: [{id: 1, name: 'Taro Yamada'}, {id: 2, name: 'Hanako Sato'}]
Devolviendo desde caché Sin llamada de API
Componente 2: [{id: 1, name: 'Taro Yamada'}, {id: 2, name: 'Hanako Sato'}]
Caché limpiado
Ejecutando nueva solicitud Llamada de API nuevamente
Datos recibidos de API
Componente 3: [{id: 1, name: 'Taro Yamada'}, {id: 2, name: 'Hanako Sato'}]

Puntos:

  • Almacenar en caché la última respuesta con shareReplay(1)
  • Múltiples componentes comparten datos (solo una llamada de API)
  • Descartar caché apropiadamente en caso de error o limpiar

Cuándo Usar

Cold

Cuándo usar

  • Cuando cada suscriptor necesita su propio conjunto de datos
  • Cuando se representa un proceso o acción recién iniciados
  • Cuando los efectos secundarios duplicados no son un problema

Ejemplos

  • Enviar una nueva solicitud POST para cada envío de formulario
  • Se necesita un temporizador diferente para cada usuario
  • Ejecutar cálculo independiente para cada suscripción

Hot

Cuándo usar

  • Cuando se comparten datos entre múltiples componentes
  • Cuando se desea ahorrar recursos (ej., reducir el número de llamadas HTTP)
  • Cuando se representan flujos de eventos
  • Gestión de estado o comunicación entre servicios

Ejemplos

  • Información de configuración compartida en toda la aplicación
  • Estado de inicio de sesión del usuario
  • Mensajes en tiempo real (WebSocket)
  • Eventos DOM (clic, desplazamiento, etc.)

Resumen

Entender y usar apropiadamente Cold Observable y Hot Observable es una habilidad importante para construir aplicaciones RxJS eficientes.

Puntos Clave

  • Cold Observable: Un flujo que comienza a ejecutarse solo después de suscribirse (ejecución independiente por suscripción)
  • Hot Observable: Comparte un flujo que ya se está ejecutando (misma ejecución para múltiples suscripciones)
  • share(): Forma más fácil de convertir Cold a Hot
  • shareReplay(): Almacenar en caché valores pasados y convertir a Hot (útil para compartir resultados de API)

Criterios de Decisión de Diseño

  • ¿Necesita compartir datos entre múltiples suscriptores?
  • ¿Es necesario almacenar en caché valores pasados y proporcionarlos a nuevos suscriptores?
  • ¿Cómo se gestionarán los efectos secundarios duplicados (ej., solicitudes HTTP)?

Basándose en estas consideraciones, seleccionar el tipo apropiado de Observable y operador le ayudará a construir una aplicación reactiva eficiente y robusta.

Secciones Relacionadas


  1. fromEvent() es Cold porque agrega un escuchador de eventos independiente para cada suscripción. Sin embargo, el evento en sí ocurre independientemente de la suscripción, por lo que se malinterpreta fácilmente como Hot. ↩︎

  2. Las Creation Functions de combinación son Cold si el Observable fuente es Cold, y Hot si es Hot. Usualmente, se combinan Observables Cold. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

Publicado bajo licencia CC-BY-4.0.