Subject vs ReplaySubject vs BehaviorSubject

I recently was helping another developer understand the difference between Subject, ReplaySubject, and BehaviourSubject. And thought that the following examples explain the differences perfectly.

Subject

A subject is like a turbocharged observable. It can almost be thought of an event message pump in that everytime a value is emitted, all subscribers receive the same value. The same analogy can be used when thinking about “late subscribers”. A Subject does not have a memory, therefore when a subscriber joins, it only receives the messages from that point on (It doesn’t get backdated values).

So as an example :

let mySubject = new Subject<number>();

mySubject.subscribe(x => console.log("First Subscription : " + x));

mySubject.next(1);
mySubject.next(2);
mySubject.next(3);

mySubject.subscribe(x => console.log("Second Subscription : " + x));

mySubject.next(4);

This will output :

First Subscription : 1
First Subscription : 2
First Subscription : 3
First Subscription : 4
Second Subscription : 4

Pretty straight forward. The first 3 values were output from the subject before the second subscription, so it doesn’t get those, it only gets new values going forward. Whereas the first subscription, as it subscribed before the first values were output, gets everything.

Now for the most part, you’ll end up using Subjects for the majority of your work. But there can be issues when you have async code that you can’t be sure that all subscriptions have been added before a value is emitted. For example :

let mySubject = new Subject<number>();
myAsyncMethod(mySubject);
mySubject.subscribe(x => console.log("First Subscription : " + x));

Imagine that “myAsyncMethod” is an asynchronous method that calls an API and emits a value on the given subject. This method may or may not complete before the subscription is added and therefore in rare cases, the subject did output a value, but you weren’t subscribed in time. These sort of race conditions on subscribing is a big cause of headaches when using plain Subjects.

ReplaySubject

That’s where ReplaySubject comes in. Imagine the same code, but using a ReplaySubject :

let mySubject = new ReplaySubject<number>();

mySubject.subscribe(x => console.log("First Subscription : " + x));

mySubject.next(1);
mySubject.next(2);
mySubject.next(3);

mySubject.subscribe(x => console.log("Second Subscription : " + x));

mySubject.next(4);

This outputs :

First Subscription : 1
First Subscription : 2
First Subscription : 3
Second Subscription : 1
Second Subscription : 2
Second Subscription : 3
First Subscription : 4
Second Subscription : 4

Notice how we get the first 3 values output on the first subscription. Then immediately as the Second Subscription joins, it also outputs the first 3 values, even though when they were emitted, the second subscriber had not yet joined the party. Then going forward, both subscribers emit the 4th value.

So what’s going on here? It’s actually quite simple. A ReplaySubject remembers the previous X values output, and on any new subscription, immediately “replays” those values to the new subscription so they can catch up. I say previous “X” values because by default, a ReplaySubject will remember *all* previous values, but you can configure this to only remember so far back.

For example :

let mySubject = new ReplaySubject(2);

This will remember only the last 2 values, and replay these to any new subscribers. This can be an important performance impact as replaying a large amount of values could cause any new subscriptions to really lag the system (Not to mention constantly holding those values in memory).

Back to our problem async code with Subject. If we change it to a ReplaySubject :

let mySubject = new ReplaySubject<number>();
myAsyncMethod(mySubject);
mySubject.subscribe(x => console.log("First Subscription : " + x));

Then it actually doesn’t matter if myAsyncMethod finishes before the subscription is added as the value will always be replayed to the subscription. Pretty nifty!

BehaviorSubject

A BehaviorSubject can sometimes be thought of a type of ReplaySubject, but with additional functionality (Or limitations depending on how you look at it).

If you think of a BehaviorSubject as simply being a ReplaySubject with a buffersize of 1 (That is, they will only replay the last value), then you’re half way there to understanding BehaviorSubjects. The one large caveat is that BehaviourSubjects *require* an initial value to be emitted.

For example :

let mySubject = new BehaviorSubject<number>(1);

mySubject.subscribe(x => console.log("First Subscription : " + x));

mySubject.next(2);
mySubject.next(3);

mySubject.subscribe(x => console.log("Second Subscription : " + x));

mySubject.next(4);

This outputs :

First Subscription : 1
First Subscription : 2
First Subscription : 3
Second Subscription : 3
First Subscription : 4
Second Subscription : 4

So again, we have the ReplaySubject type functionality that when the second subscriber joins, it immediately outputs the last value of 3. But we also have to specify an initial value of 1 when creating the BehaviorSubject.

But why is an initial value important? Because you can also do things like so :

let mySubject = new BehaviorSubject<number>(1);
console.log(mySubject.value);

Notice we can just call mySubject.value and get the current value as a synchronize action. For this to work, we always need a value available, hence why an initial value is required. Again, if you don’t think that you can provide an initial output value, then you should use a ReplaySubject with a buffer size of 1 instead.

Also, just a quick warning on BehaviorSubjects, this might be one of those times where spelling trips you up if you are not American. For example if you are getting the warning :

Cannot find name 'BehaviourSubject'

Just remember it’s Behavior not Behaviour!

Leave a Reply

Your email address will not be published. Required fields are marked *