Operators like publish(), refCount(), share() make it easy to convert a cold Observable to a hot one, and are often necessary to get some feature done. In this lesson we will learn when exactly do we need to convert to hot, and when can we leave the Observable cold.
[00:02] If you have used RxJS long enough, you have probably heard of the idea of cold and hot observables. It can be confusing, but we're talking about how to make a shared execution of the same observable from multiple subscribers.
[00:14] The simplest example of this is when you have a clock observable that ticks every second, and we have six of those events. Now we have here two subscribers, a and b. Once we run that, each of those are going to get a different execution. If this is familiar to you, this is the usual problem.
[00:34] We solve that by sharing that execution. Here, we can add .share() at the end, and now we're going to have a common execution of this clock observable shared to these two different subscribers. Now they're going to see the same events at the same time, like that. It's easy to stick to .share() without knowing what it does. I've seen cases where everything is .shared() where it doesn't need to be.
[01:03] In order to avoid sharing everything, it's easy to remember that subscribe means invoke the execution of this collection so we can observe it. Without .share(), we're saying here, "Please, I want the values that come from this collection and deliver them here." Of course, if we say subscribe twice, we're invoking two different executions of that.
[01:29] If you keep this in mind, it makes it much easier, because when we add .share() here, we're saying, "Instead of invoking a new execution, please share the execution of the observable if it exists."
[01:46] Let's look at a bit more difficult example where it may be tempting to .share() everything. Here we also have a clock observable, and we have another observable called random numbers. We're mapping each of those to a random number. Then we have two other observables -- one of them gets random numbers smaller than 50, and the other gets random numbers larger than 50.
[02:13] ToArray() simply collects all of them in an array so that we see all of them together at the end when it completes. Let's run this and see what happens. The clock is generating events over time. Then we're mapping each of those to a random number. When that completes, we collect all of those numbers into small and large collections.
[02:33] Notice we have some problems here. We have four numbers in total in small and large category, but we had originally six random numbers, as you can see here. Also, these numbers don't actually occur in this original collection, and neither does 57.84 happen here. This is very strange. You may find this type of behavior in an RxJS app, so it's usually the case that we're not sharing executions here.
[03:05] What's going on here? First of all, when we do subscribe, remember we're invoking, "Please, give me an execution of this collection." When we do this, we're going to look here, "Give me an execution of this collection. Give me an execution of that collection. Give me an execution of this random number," which will finally ask for an execution of the clock.
[03:29] This is going to have its own execution. Each of these maps will be totally exclusive for large number subscriber and the same thing for small number and random number. We essentially have here three executions of the clock and three executions of the random number. That's why we have all of these different random numbers.
[03:55] It's easy to instead put .share() here everywhere. Sometimes people do this like that in an attempt to ignore where the source is and try to share everything. To some extent, this may solve the problem. As we see here, all of these numbers correspond there, and they're the same, but this is an overkill, and we don't need to do this. We can just share what is necessary.
[04:29] Let's look at what is it that we need to share exactly. It all has to do with what kind of side effects happen once we subscribe or once something happens in this operator. For instance, this operator will always give out the same output for the same input. For instance, if we give x is 20 here, it's always going to say true.
[04:54] On the other hand, when we invoke this function multiple times, we get different numbers every time. This is a side effect. We probably want to share this part, for instance. Also, once we subscribe to an interval, you know that internally it will do setInterval(). That's also like a side effect because it's creating a pseudo-thread. It's creating that thing, so it's not so pure.
[05:21] We could probably already conclude that this part needs to be shared, because once we call setInterval(), we want that to be shared. Then take(6) will always do the same thing, so we don't need to share that. That's not enough, because let's see what happens.
[05:41] We get the numbers 61, 8, 3, 85, but those numbers are not corresponding here. The reason for that is that once we call a small number subscribe(), we're saying, "Please give me the execution of this." Then it's going to invoke the execution of this, and then finally invoke the execution of this, and invoke the execution of this map based on this clock.
[06:06] Even though we're going to share the same setInterval(), but we're not sharing this map, so each of these subscribers is going to get a different execution of this map operation. That's why we also need to share this part. Then, once we do that, we see 81, 30, 60, 76, and those numbers appear here.
[06:33] It's a good idea not to call share() everywhere because every time you call share() you're actually creating a new subject, and that might cause maybe problems with garbage collection. It's generally wise not to do operations that we don't need to do in the first place.
[06:51] The tip here in order to avoid cold and hot problems is to remember that subscribe means invoke an execution of this collection, and then all of these executions are changed throughout these operators. Some of those executions we want to share, such as, "This random number, I wanna share it," or, "The setInterval() that's inside this creation, I wanna share that as well."
[07:18] The other bits are going to behave exactly the same way no matter if you share them or not. That's why we don't do it. Over time, you get to develop a sense of what should be shared and what should not be shared, depending on these side effects that may occur.
Hi cyberdyme, that's in order to share the setInterval with multiple subscribers. Without share there, you would notice the problem once a second subscribe happens at a different time (say 200 ms) after the first subscribe, because you would have two setInterval timers ticking at different times and producing random numbers.
I find that a bit confusing only because it seems like you're suggesting that it's required in order to get this specific example to work correctly. Assuming that the clock$
variable is private and internal (and if we could somehow guarantee no other subscribers to that observable) it should be enough just to share the result of the map
on the random$
and be done.
I think the suggestion may be that since clock$
is independently available for subscription elsewhere that share
is a good idea, but technically it's not required for this example. Is that correct?
More specifically, if random$
was defined using Rx.Observable.interval(500).take(6)
directly instead of in terms of clock$
then only one share
is needed after the random number calculation -- not after both the take(6)
AND the random mapping.
As always, though, these videos are awesome. Thanks for putting them together.
Hi Mike. Yes your thought process is correct. The idea in this lesson was showing how some operators (like map or filter) never need to be shared, because they are pure and yield the same results independent of subscriptions. Some operators or factories are impure, and these may need to be shared. The example in question is of course contrived, but the lesson is to identify the impure parts that may need sharing.
To heed to the lessons of this video, would events streams associated with input such as mouse theoretically be a candidate of also making sure is shared once multiple subscribers are involved?
Hi Kevin. Typically if we share an Observable that already represents a hot source, like mouse clicks, the result behavior stays the same. That said, it may still be wise to share that Observable since then you avoid adding multiple event listeners to the underlying DOM API (element.addEventListener()
).
Hi, like your courses very much! Here I am still not quite get why clock$ also need to add .share() :(
Q: If I share randomNum$, does it mean clock$ also get shared?
Get bit more confused about "second subscribe happens... say 200ms) after the first subscribe..... two setInterval timers ticking...."
Q: If the source is not yet completed, no matter how many subscribers, they still share the same source, right? So after 200ms of first subscriber, the source is not yet completed... confused why there are two timers?
Hi Zhentian! I'm here to help.
The first question you made should be answered above in this thread, someone else had the same question.
The second question can be answered as: if the cold source is not yet completed, each subscriber will invoke its own exclusive execution of the source, so there is no sharing. This is fundamentally what cold means: no sharing happens at all. For more information on the topic of hot and cold, you can read this https://staltz.com/cold-and-hot-callbacks.html
Very good explanation! I couldn't get why we have to share interval Observable, I thought that just the randomNum$ would do the trick.
You don't need a .share() on the clock$ observable. The .share() on the randomNum$ observable is sufficient for this example.
Great course. Thank you Andre for that stuff.
Why not just have a single share after random, why also in the set interval