Hot vs Cold Observables
Hot vs Cold Observables

In the daily work of Angular developers, RxJS and observables are constant companions. Here, I’ll clarify what hot and cold observables mean and how they differ. Let’s start with a few working definitions of these two concepts. 

  • An observable is labeled cold when its data source is generated within it. If the source is instead established outside the observable, we call it a hot observable.
  • Cold observables operate on a unicast model, while hot observables follow a multicast model.
  • Emissions from cold observables begin only after a subscription is made. Hot observables, however, emit continuously—even when no one is subscribed.

Don’t let these definitions trip you up. They’ll become clearer once we build a useful mental picture.

Think about YouTube videos as a real-world comparison. Each viewer of a video gets an independent playback session, so they can start watching on their own schedule. This mirrors how cold observables function. A live stream, in contrast, works differently. It aligns with a hot observable, being shared by all viewers simultaneously.

const video$ = of(null).pipe(map(() => Math.random()));
video$.subscribe(console.log); // Viewer 1
video$.subscribe(console.log); // Viewer 2
video$.subscribe(console.log); // Viewer 3

What output would you anticipate in the console?

The console shows three distinct results! Each subscriber gets its own execution environment, meaning the data is unicast. The result of Math.random() isn’t shared among them. That’s a straightforward demonstration of cold observable behavior. So, how can we switch this pattern from cold 🧊 to hot 🔥?

const video$ = of(null).pipe(
  map(() => Math.random()), 
  shareReplay() // <-- makes the observable hot
);
video$.subscribe(console.log); // 0.9737887545048507
video$.subscribe(console.log); // 0.9737887545048507
video$.subscribe(console.log); // 0.9737887545048507


By applying shareReplay(), we transform the cold observable into a hot one, so the randomly generated value is pushed out to every subscriber, ensuring they all receive the identical result.

What makes shareReplay turn an observable hot is its internal reliance on ReplaySubject, and in RxJS, any Subject is classified as hot. This behaves similarly for other hot operators, including share, except that operator uses a plain Subject internally instead.

A further well-known operator capable of generating hot observables is fromEvent(). It doesn’t rely on any RxJS Subjects internally, but the event source is always available rather than being instantiated with each subscription—this serves as the primary characteristic defining an observable as hot. (This distinction will become more evident once we build a custom operator for hot/cold cases later)👇 However, for now…

What effect does this have on actual ANGULAR code?

To illustrate, HTTP requests produce cold observables. When we subscribe to such an observable twice in a template using the async pipe, it results in two separate server calls. To avoid these redundant requests, we need to convert that cold observable into a hot one. Using the shareReplay() operator, as discussed above, accomplishes this, ensuring only a single server request is made.

@Component({
  selector: "app-root",
  template: `
    <main>
     <h1>RxJS - Hot / Cold Observable</h1>
     <div>Count: {{ (posts$ | async)?.length }}</div>
     <ul>
       <li *ngFor="let post of posts$ | async">{{ post.title }}</li>
     </ul>
    </main>
  `,
  styles: [``],
})
export class AppComponent implements OnInit {
  posts$!: Observable<any[]>;
  constructor(private http: HttpClient) {}

  this.posts$ = this.http
    .get<any[]>(`http://jsonplaceholder.typicode.com/posts`)
    .pipe(shareReplay());
}

Building a custom hot and cold operator

Another way to deepen our intuition about the connection between a data source and whether an observable is hot or cold is to craft our own creation operator. In RxJs, creation operators are simply functions that return an Observable. The example operator below yields a random number from 1 to 6, simulating a dice throw. 🎲🎲🎲

const rollDice = (): Observable<number> => {
  return new Observable((subscriber) => {
    const diceNumber = Math.floor(Math.random() * 6 ) + 1; // inside
    subscriber.next(diceNumber);
  });
}

const dice$ = rollDice();
dice$.subscribe(console.log); // 5
dice$.subscribe(console.log); // 3
dice$.subscribe(console.log); // 2

In the code snippet above, the diceNumber source is generated within the Observable. Consequently, it remains unicasted—each subscriber receives a distinct value. To turn the rollDice operator into a hot one, we only need a single adjustment: relocate the source creation outside the Observable.

const rollDice = (): Observable<number> => {
  const diceNumber = Math.floor(Math.random() * 6 ) + 1; // outside
  return new Observable((subscriber) => {
    subscriber.next(diceNumber);
  });
}

const dice$ = rollDice();
dice$.subscribe(console.log); // 4
dice$.subscribe(console.log); // 4
dice$.subscribe(console.log); // 4

At this point, the diceNumber value is stored in cache, enabling multicast behavior. As a result, every future subscriber receives an identical result.

That wraps up today's content. I trust you found it enjoyable and picked up a few insights. There's also a video version of this material just beneath. Don't hesitate to leave feedback, spread the information, and join the conversation in the comments area.

Hot vs Cold Observable in RxJS — figure 2

Advanced Angular Forms – Deep Dive


Check out the most Advanced Angular Forms course made by Google Developer Expert in Angular