publishBehavior: A Closer Look

The publishBehavior operator is closely related to the publish operator, but with one key difference: it relies on a BehaviorSubject internally rather than a plain Subject. This operator enables a single shared subscription to the source stream among multiple observers, while also replaying the most recent value emitted prior to the stream’s completion.

Consider this illustration where publishBehavior coordinates a single subscription to the underlying obs stream across three subscribers. To highlight the sharing mechanism, we’ve built a custom Observable that logs each emitted value:

const log = (index) => (v) => console.log(`subscription ${index}\t: ` + v);
const logCompleted = (index) => () => console.log(`subscription ${index}\t: ` + 'completed');

const obs = new Observable(producer).pipe(
   take(3),
   tap({ complete() { console.log('underlying stream completed') }})
);

const shared = obs.pipe(
   publishBehavior(null)
) as ConnectableObservable<any>;

shared.subscribe({next: log(1), complete: logCompleted(1)});
shared.subscribe({next: log(2), complete: logCompleted(2)});

shared.connect();

setTimeout(() => shared.subscribe({next: log(3), complete: logCompleted(3)}), 500);

Just like multicast, the publishBehavior operator returns a ConnectableObservable that must be linked to the source stream.

In the example, the first two observers subscribe before the shared observable gets connected, so they both receive all 4 values, including the initial null that BehaviorSubject requires. The third subscription is introduced with a timeout, arriving after the first two values have already been emitted. As a result, it picks up the cached value 1 and then catches the final value 2 right before the source stream completes.

Here’s what the console shows:

subscription 1	: null
subscription 2	: null
producer		: 0
subscription 1	: 0
subscription 2	: 0
producer		: 1
subscription 1	: 1
subscription 2	: 1
subscription 3	: 1
producer		: 2
subscription 1	: 2
subscription 2	: 2
subscription 3	: 2
underlying stream completed
subscription 1	: completed
subscription 2	: completed
subscription 3	: completed

You can think of publishBehavior as a convenient shorthand for the multicast operator paired with a BehaviorSubject. To fully grasp publishBehavior, it helps to understand the mechanics of multicast. In fact, swapping publishBehavior for multicast in this manner yields identical results:

const shared = obs.pipe(
   multicast(new BehaviorSubject(null))
);

It’s worth noting that, unlike publishReplay, when a subscription starts after the source stream has already completed, publishBehavior does not deliver the cached value the way publishReplay would. Instead, it forwards only the COMPLETE or ERROR notification.

The following example mirrors the earlier setup: one shared subscription to the obs stream across 3 subscribers. However, this time the 3rd subscription is delayed with a timeout and takes place after the source stream finishes, so it only receives the COMPLETE signal:

const obs = interval(200).pipe(
   take(3),
   tap({ complete() { console.log('underlying stream completed') }})
);

const shared = obs.pipe(
   publishBehavior(null)
);

shared.subscribe({next: log(1), complete: logCompleted(1)});
shared.subscribe({next: log(2), complete: logCompleted(2)});

shared.connect();

setTimeout(() => shared.subscribe({next: log(3), complete: logCompleted(3)}), 1000);

The resulting output is:

subscription 1	: null
subscription 2	: null
subscription 1	: 0
subscription 2	: 0
subscription 1	: 1
subscription 2	: 1
subscription 1	: 2
subscription 2	: 2
underlying stream completed
subscription 1	: completed
subscription 2	: completed
subscription 3	: completed

As the output demonstrates, the 3rd subscription received only the COMPLETE notification and none of the values held by the BehaviorSubject.

In essence, publishBehavior—like multicast—simply inserts a BehaviorSubject into the observable pipeline. It’s the BehaviorSubject that does the heavy lifting for sharing, not the publishBehavior or multicast operator itself. Both operators are just conveniences for working with Subjects.

That said, multicast offers more flexibility: you can supply any Subject instance of your choosing. With publishBehavior, the underlying BehaviorSubject is created behind the scenes and is never exposed.

Keep in mind that mulicast is often more powerful because it accepts a factory function that can generate a BehaviorSubject instance on demand, multiple times, rather than the single internal instance used by publish-style operators. For more details on this, check out this explanation.

In the example below, using multicast with a factory changes the output because the underlying subscription gets restarted:

const obs = interval(200).pipe(
   take(3),
   tap({ complete() { console.log('underlying stream completed') }})
);

const shared = obs.pipe(
   multicast(() => new BehaviorSubject(null))
);

shared.subscribe({next: log(1), complete: logCompleted(1)});
shared.subscribe({next: log(2), complete: logCompleted(2)});

shared.connect();

setTimeout(() => { shared.subscribe({next: log(3), complete: logCompleted(3)}); shared.connect() }, 1000);

Running this code produces the following console output:

subscription 1	: null
subscription 2	: null
subscription 1	: 0
subscription 2	: 0
subscription 1	: 1
subscription 2	: 1
subscription 1	: 2
subscription 2	: 2
underlying stream completed
subscription 1	: completed
subscription 2	: completed
subscription 3	: null
subscription 3	: 0
subscription 3	: 1
subscription 3	: 2
underlying stream completed
subscription 3	: completed

Observe that the 3rd subscription receives all 4 values (including the initial null), and the source stream completes twice in total:

subscription 3	: null
subscription 3	: 0
subscription 3	: 1
subscription 3	: 2
underlying stream completed
subscription 3	: completed

That’s the impact of the multicast operator when used with a factory function.