There are no simply "hot" Observables in RxJS
Wednesday 31 January 2024 β’ π 11 min read β’ back to Blog β’ edit on Github
Photo by Pawel Czerwinski on Unsplash
So I caught your attention? π Good! Because I want you to look at the "HOT and COLD" Observables concept from a different perspective... it's one of the most confusing oversimplifications I've had to deconstruct over and over again since quite a long time.
Okay, to be fair... it is quite useful when we introduce RxJS to a complete beginner. But as soon as they dive into real project usecases, e.g. late subscription of a newly rendered component that subscribes to a (behavior? replay?) subject which had already emitted something - then the "hot and cold" concept is... harmful.
The following aphorism illustrates it quite well:
All models are wrong, but some are useful.
However, I would add: useful, but until a certain point!
Table of Contents
- Common ground
- False dichotomy
- at least 32 different types of hot Observables? π²
- Sh*t, what are all those things?! π
- What? Please, stop π₯
- First things first
- Fact Summary
- Back to π₯ hot 32 π₯
- Where did you get these from?
Common ground
Before we move on let's make sure we're all on the same page. What is a hot observable? The Rxjs.dev official docs define it the following way:
An observable is "hot", when its producer was created outside of the context of the subscribe action. [...]
Some people
β οΈ BTW there seems to be some belief π as if hot Observables are those Observables which "emit infinite amount of values". That's pure nonsense.
False dichotomy
The "problem" with the "hot and cold" concept is that it implicitly suggests that there are two types of Observables. Only two. And yes, when you read the explanations (like this legendary one by Thoughtram), you know that reality is more complex π but when I join a team in order to do some frontend consultancy services (or run a Angular/Rx training), developers most often think in either-or. Either hot or cold.
I hope you are one of those truly passionate devs who dig into things, dive really deep, seek for understanding. You might already know that there are dozens of completely different types of "hot". But the majority of the industry isn't like this. We're all responsible for the concepts and metaphors we introduce.
So it depends on how we count these things... but as much as we dive into Rx internals, we'd find out there are at least 32 different types of hot streams.
at least 32 different types of hot Observables? π²
Yep, you heard it right. In very short, 32 is a Cartesian product of:
- 4 types of subjects (4)
- reset on error, boolean (2)
- reset on complete, boolean (2)
- reset on ref count 0, boolean (2)
4*2*2*2 = 32
If we'd also take connectable streams into account (as in Understanding publish, refCount and connect), there'd be even more π.
Sh*t, what are all those things?! π
These are separate characteristics of Subjects that have existed for a very long time already - and most probably you experienced them in some rather unclear circumstances, and haven't become aware of what's going on. For example, you might have noticed that, even though the reference counter (number of active subscribers) have
dropped to zero, share()
used to recreate the underlying stream (e.g. a new HTTP request) whenever a new active subscription happened. But after replacing the share()
operator with shareReplay()
, it didn't!. Even though the circumstances were the same... And this is NOT a specificity of either Subject or
ReplaySubject. This is what so called "hot" is underneath. One term is just so wrong, if you want to get your head around it!
What? Please, stop π₯
No worries, by the end you'd understand all of that. Stay with me. And I hope you'd see that, indeed, "hot and cold" is a seriously harmful oversimplification.
First things first
We need to remind a couple of facts.
Fact 1: Notification Types
Observable streams don't just emit values. Strictly speaking, there are 3 types of notifications:
- next(value)
- error(error)
- complete() and all operators, subjects, etc - everything in Rx processes all of them. Sometimes they just pass the notification through down to the downstream, sometimes they do more interesting things - but all 3 notifications are always involved.
Fact 2: Unicasting and Multicasting
All streams can typically serve only one consumer - all operators, higher-order streams, etc. Being able to emit notifications only to one target is called Unicasting.
... all, except for Subjects - these are the only ones which can serve multiple consumers (and it's actually the main point why they exist...). This "ability" is called Multicasting.
Fact 3: observables are lazy by default
It's a quite well known fact that, until an observable gets subscribed to, it doesn't emit a value. That's fair, since why producing items, if there'd be no one to consume them? Only when they're is some subscriber interested in the data, it makes sense to produce and emit the notifications.
Yep, produce...
Get notified about blog posts, recordings and workshops!
Subscribe to Ducin.dev and you'll get notified when new content is published. Expect in-depth posts, recordings and online workshops. The topics include: JS/TS, Angular, React and related tech.
Fact 4: producer-consumer pattern
Let's take a look at the following stream:
second$ = interval(1000).pipe(
map(x => x+1)
);
With no subscribers, the observable emits no events. But once the first subscriber arrives, a new interval (native setInterval
) gets created. However, when another subscriber arrives, there'd be a separate, competing interval.
Certainly, observables are clearly lazy in this case. But what I also want to highlight is that a stream like this one:
second$ = interval(1000).pipe(
map(x => x+1)
);
isn't actually something which is capable of emitting anything. NOT YET...
Now, be cautious. The difference is not only with naming, but also with semantics.
An observable - is something we can observe (subscribe to). Clear.
However, a stream is something that emits values. Now let's get back to the point in time when we had no subscribers: any interval hasn't been created YET. So we've got composition, we've got a stream - but it's dead. There's nothing live that could actually turn the stream on.
Streams do implement the producer-consumer model. Depending on the scenario, there might be either 1 producer per 1 subscriber (cold), or 1 producer per multiple subscribers (at least 32 types of hot). There could be even more complex, though very rare scenarios, where multiple producers use a common subject (sink, proxy) which then passed all notifications through to all subscribers (many-to-many).
A producer could be anything that is the source that start emitting anything: or could be an interval or a timer, but also HTTP request, event listener, or a websocket connection.
Fact Summary
So a subject is multicasting events down to multiple subscribers. In a typical scenario there's one shared producer underneath (could be more, but that's rare).
Without any subject, the stream will unicast. For each subscriber, a new producer would be created instantly.
Back to π₯ hot 32 π₯
So far so good, we've got the knowledge to champion all the possibilities.
All these apply to situations where we do have a subject ("hot", after all).
reset on error
If any of the producers emits an ERROR
notification down to the subject - should it reset? Reconnect? Or - in our terminology - should a new producer be recreated?
reset on complete
Similar as above, but applies to COMPLETE
notification.
reset on ref count zero
Imagine the subject initially had no subscribers. Then first subscriber came, then the second one. The subject counts all active subscribers all the time. Now, the subscribers go away, and as they unsubscribe, the counter (ref count) goes down.
And the question arises: if the refCount
drops to zero (no active subscribers), should we keep the connection to the producer active? In other words - shall we keep the producer live?
Remember, of the producer is live, it will keep on emitting items according to its nature (e.g. interval will keep on doing its job).
Where did you get these from?
Now comes my favorite part π - source code: let's dive into RxJS ShareReplay
sources. This one is especially good to look at, add it specifies config parameters exposed by the share
operator:
shareReplay
is just a specialized version of share
. Apart from accepting config (as input parameters) and preparing the new config (to be passed to share
) - apart from that, shareReplay()
just calls share()
.
connector
is a function that returns a subject - with whatever type, potentially even having some subscribers already (not common, though possible).
And the resetOnError
, resetOnComplete
and resetOnCountZero
are things we've already gone through πAll in all, we can determine all these.
How important is reading the source code?
Sometimes not that much. But this time - extremely important! Imagine all these situations where you use share()
/shareReplay()
and you hope it should be ok. NO! Think about all these usecases, such as switching routes (navigation), potentially causing refCount
zero for a short while. Or receiving complete
which
might unintentionally break your app logic.
How hot π₯ are your streams?
Being hot is about multicasting (sharing the connection with the producer or, in practice, sharing the producer). But I hope your agree with me that the following scenarios:
- sharing the same producer no matter what
- and/or sharing the producer unless
RESET
emitted - and/or sharing the producer unless
COMPLETE
emitted - and/or sharing unless all subscribers are gone
... all have completely different runtime behavior. Failing to acknowledge the difference might lead to serious bugs and/or unintentional behavior.
One more case
As previously said, there are more than 32 different types of "hotness". How come? We already mentioned connectable observables. But there's one more, slightly exotic, though technically doable. And that is: an observable that gets the values from a producer who has been created totally outside of the context of any observables whatsoever. Imagine just a producer which, whenever creating a new thing, it calls a function - it will keep on doing that no matter if any observables and/or subscribers are there or not. If a new subscriber arrives, the void function just gets replaced (or added, depending on the implementation) to feed the subscriber. Anyway, the lifespan of the producer doesn't depend on subscribers and, especially, producer is not created within the subscription context.
That's just an exotic example, but all in all, don't stick to the 32
number. Remember that there are dozens of different flavors of "hotness".
Conclusion
The terms unicasting/multicasting precisely define whether a subject is being used or not. Only this is already way more precise than "cold and hot" distinction.
A cold stream is a lazy, unicasted stream which creates a new producer per each subscriber.
But when it comes to hot streams, they could be lazy or not (remember Connectable), they could share the producer unless some circumstances happen. Last but not least, different subject types apply completely different emission strategies. It's all so damn different that it's pure madness to throw it all into one bag. One bag that discourages many people from diving deeper.
So now, whenever you need to introduce a Subject, don't just choose between share()
and shareReplay
(also trying to figure out why is there no shareBehavior()
one?!). Think about all 32 conditions - one of those is the one you're looking for, e.g
stream$.pipe(
share({
connector: new BehaviorSubject("INITIAL"),
resetOnError: true,
resetOnComplete: false,
resetOnRefCountZero: false,
})
)
And when we revisit the official definition of a "hot observable":
An observable is "hot", when its producer was created outside of the context of the subscribe action. [...]
the definition determines neither the behavior nor the lifespan of the producer, which make a huge difference.
My strong recommendation: as soon as one has grasped the idea of Observables, use more precise terms and stop using the ambiguous "hot" (and cold) streams metaphor. It introduces false dichotomy and pretends that things are simple.
The more we know, the more we understand they aren't.
I want to give special thanks to Jan-Niklas Wortmann and Bartosz Cytrowski for proof reading this article πͺ