A topic that seems to confuse a lot of devs is the distinction between hot and cold Observables in RxJS. Already when we had a look at Observables in general, there was a short intro to hot and cold Observables. However, in this article, we want to deep dive.
Official RxJS definitions
The rxjs.dev website provides definitions on the glossary page. Therein a cold Observable is defined as follows:
An observable is “cold” when it creates a new producer during subscribe for every new subscription. As a result, “cold” observables are always unicast, being one producer observed by one consumer. Cold observables can be made hot but not the other way around.
https://rxjs.dev/guide/glossary-and-semantics, December 2023
The definition for the hot Observable is:
An observable is “hot”, when its producer was created outside of the context of the subscribe action. This means that the “hot” observable is almost always multicast. It is possible that a “hot” observable is still technically unicast, if it is engineered to only allow one subscription at a time, however, there is no straightforward mechanism for this in RxJS, and the scenario is an unlikely one. For the purposes of discussion, all “hot” observables can be assumed to be multicast. Hot observables cannot be made cold.
https://rxjs.dev/guide/glossary-and-semantics, December 2023
Let us have a look at what it means in detail.
What is a producer?
A producer is a usually asynchronous functionality that “generates” values. DOM Events, HTTP calls, random number generators or a route change event in Angular can all be producers. Let us create a simple producer by hand:
const producer = setInterval(() => {
console.log(++currentNumber);
if (currentNumber === 10) {
done();
}
}, 1000);
The above code produces numbers every second. An Observable is simply a function that connects an Observer to a producer. Let us try to connect this producer to an Observer in the next section.
Subscribing to a cold Observable
We are creating a custom Observable below that is based on the example from the previous section. The producer function is more or less the same, although in this case the created number is passed to the observer
. The Observable returns a function that stops the producer. This is the function called when unsubscribing.
const obs$ = new Observable((observer) => {
let currentNumber = 1;
const producer = setInterval(_ => {
observer.next(currentNumber++);
}, 1000);
return () => clearInterval(producer)
});
const subscription = obs$.subscribe(console.log);
setTimeout(_ => {
subscription.unsubscribe();
done();
}, 5_100);
// this logs:
// 1, 2, 3, 4, 5
In a basic test, we see that a subscriber would just receive the numbers 1, 2, 3, 4, and 5 separated by a 1s waiting time. This is the cold Observable case. And this becomes especially obvious if we let a second Observer subscribe to the same Observable:
const subscription = obs$.subscribe({ next: v => console.log('1st: ' + v)});
let subscription2;
setTimeout(() => {
subscription2 = obs$.subscribe({ next: v => console.log('2nd: ' + v)});
}, 1000);
setTimeout(_ => {
subscription.unsubscribe();
subscription2.unsubscribe();
done();
}, 6_100);
This logs every number for each of the two Observers:
1st: 1
1st: 2
2nd: 1
1st: 3
2nd: 2
1st: 4
2nd: 3
1st: 5
2nd: 4
1st: 6
2nd: 5
For the 1st Observable also the number 6 is logged as the subscription to the producer function is only cancelled after 6 seconds. So, you can see that each Observer gets its own producer. As the official documentation was already stating: “one producer observed by one consumer”.
Subscribing to a hot Observable
Now we need a little bit more code around the data source that is producing the values:
const producer = (function() {
let currentNumber = 1;
const consumers: { (data: number): void; } [] = [];
const intervalId = setInterval(_ => {
currentNumber++;
consumers.forEach(c => c(currentNumber));
}, 1000);
return {
register: (consumer) => {
consumers.push(consumer);
},
cleanUp: () => clearInterval(intervalId)
}
})();
The above producer is a functionality producing values the moment its code is running. Every second a new number is produced. Based on it we create an Observable similar to before:
const obs$ = new Observable((observer) => {
producer.register((v) => observer.next(v));
});
This time the Observable shares the data source. The producer is instantiated and running outside of the Observable. The producer is already running independently of any Observable subscription. It is “hot” because it is already working and producing values.
Let us subscribe to this hot Observable and see what values we are getting:
const subscription = obs$.subscribe({ next: v => console.log('1st: ' + v)});
let subscription2;
setTimeout(() => {
subscription2 = obs$.subscribe({ next: v => console.log('2nd: ' + v)});
}, 2000);
The first subscriber is added immediately in our code while the second one is only added after 2 seconds. The values that are logged to the console are:
1st: 2
1st: 3
2nd: 3
1st: 4
2nd: 4
1st: 5
2nd: 5
1st: 6
2nd: 6
We can see that the subscriptions do not influence the producer. Both subscribers miss the value 1 because the Observable is hot and its producer already running. The Observer 2 even misses more values because it subscribes later.
On the other hand, the “same” value ends up at multiple Observers. So, the connection to the producer is shared and the delivery of values is multicast. The above example was a bit of a hacky way to demonstrate the principle behind hot Observables. But I hope you got it.
Making a cold Observable hot
As seen in the definition it is possible to make a cold Observable hot. How do we share an Observable? We just use the share operator:
const obs$ = new Observable((observer) => {
let currentNumber = 1;
const producer = setInterval(_ => {
observer.next(currentNumber++);
}, 1000);
return () => clearInterval(producer)
}).pipe(share());
This makes the stream hot and a new (late) subscriber is reusing the producer. That means if an observer subscribes after 1 second to obs$
it is not going to see the value 1.
Why do we need cold and hot Observables in RxJS?
In the previous sections, you have seen the differences between cold and hot Observables. Let us summarise it and explain why we need both variants:
- Cold Observables create a producer during subscription and every subscriber receives its own producer.
- Hot Observables have the producer created outside of the stream or Observable.
- Observables are just functions that connect an observer to a producer.
This means that for a certain use case we need to choose one or the other variant:
- For every cold Observable, a new producer functionality is triggered. This can be resource-intensive in some cases and the required behavior in other cases.
- Hot Observables allow you to share the produced values to get some load off the producer functionality.
- The operators share and shareReplay allow us to make a cold Observable hot.
Exercise for the RxJS hot vs. cold Observable topic 💪
What about shareReplay? Does it make an Observable hot? The code below might help you to understand.
let numerOfValuesProduced = 0;
const obs$ = new Observable((observer) => {
let currentNumber = 1;
const producer = setInterval(_ => {
numerOfValuesProduced++;
observer.next(currentNumber++);
}, 1000);
return () => clearInterval(producer)
}).pipe(shareReplay());
This post is part of the RxJS mastery series. As always the code examples can be found on GitHub.