Using share
and shareReplay
is pretty darn confusing. The way share
and shareReplay
work is not always obvious and might lead to unexpected behavior in your application.
Fortunately, you have found this article and after reading you’ll understand the differences between share
and shareReplay
.
share
The
share
operator will multicast values emitted by a source Observable for subscribers.
Multicast means data is sent to multiple destinations.
As such, share
allows you to avoid multiple executions of the source Observable when there are multiple subscriptions. share
is particularly useful if you need to prevent repeated API calls or costly operations executed by Observables.
The slightly modified code from the official documentation below has a shared source Observable that emits random numbers at 1-second intervals, up to two emissions. You can also run the example on StackBlitz.
import { interval, tap, map, take, share } from 'rxjs';
const source$ = interval(1000).pipe(
tap((x) => console.log('Processing: ', x)),
map(() => Math.round(Math.random() * 100)),
take(2),
// if you remove share, you will see that
// each subscription will have its own execution of the source observable
share()
);
source$.subscribe((x) => console.log('subscription 1: ', x));
source$.subscribe((x) => console.log('subscription 2: ', x));
setTimeout(
// this subscription arrives late to the party. What will happen?
() => source$.subscribe((x) => console.log('subscription 3: ', x)),
1500
);
/* Example Run
### share operator logs:
--- 1 second
Processing: 0
subscription 1: 33
subscription 2: 33
--- 2 seconds
Processing: 1
subscription 1: 12
subscription 2: 12
subscription 3: 12
### without share operator logs:
--- 1 second
Processing: 0
subscription 1: 55
Processing: 0
subscription 2: 65
--- 2 seconds
Processing: 1
subscription 1: 64
Processing: 1
subscription 2: 2
--- 2.5 seconds
Processing: 0
subscription 3: 42
--- 3.5 seconds
Processing: 1
subscription 3: 95
*/
share 's Inner Observable: Subject
When you subscribe to a shared Observable, you are actually subscribing to a Subject exposed by the share
operator. The share
operator also manages an inner Subscription to the source Observable. The inner Subject
is the reason multiple subscribers receive the same shared value, as they are receiving values from the Subject
exposed by the share
operator. See previous example on StackBlitz.
share's RefCount
share
keeps a count of subscribers. Once the subscriber count reaches 0
, share
will unsubscribe from the source Observable and reset its inner Observable (the Subject
). The following (late) subscriber will trigger a new Subscription to the source Observable, or in other words, a new execution of the source Observable. Here’s an example of this behavior, also available on StackBlitz.
import { defer, delay, of, share, shareReplay, tap } from 'rxjs';
const source$ = defer(() => of(Math.round(Math.random() * 100))).pipe(
tap((x) => console.log('Processing: ', x)),
delay(1000),
// shareReplay({ bufferSize: 1, refCount: true }),
share()
);
source$.subscribe((x) => console.log('subscription 1: ', x));
source$.subscribe((x) => console.log('subscription 2: ', x));
setTimeout(
() => source$.subscribe((x) => console.log('subscription 3: ', x)),
3500
);
shareReplay
In some cases what you really need is a share
that is able to behave as a BehaviorSubject would. For example: if a cold Observable has a share
operator, like the code example above, a late subscriber to it would never get the values emitted before the subscription because it subscribed after share
operator reached refCount
0, which means the share
operator unsubscribed from the source Observable and reset its inner Subject. The late subscriber would thus subscribe to a new inner Subject, which runs a new execution of the source Observable, in this case that means a second API call: exactly the opposite of what you really needed.
That's why shareReplay
exists: it both shares the source Observable and replays the last emissions for late subscribers.
Also, it does not keep a count of subscribers by default, but you may use the refCount option with a true
value to enable that behavior.
shareReplay's Inner Observable: ReplaySubject
In contrast to share
, shareReplay
exposes a ReplaySubject
to subscribers. ReplaySubject(1)
is very similar to a BehaviorSubject
.
shareReplay's RefCount
Since shareReplay
does not keep track of a subscriber count by default, it is not able to unsubscribe to the source Observable. Ever. Unless you use the refCount
option.
In order to use shareReplay
while getting rid of memory leak issues you can use bufferSize
and refCount
options: shareReplay({ bufferSize: 1, refCount: true })
.
shareReplay
never resets its inner ReplaySubject
when refCount
reaches 0, but does unsubscribe from the source Observable. Late subscribers will not trigger a new execution of the source Observable and will receive up to N (bufferSize)
emissions. Play with the previous example on StackBlitz to see the difference.
Use with Caution
In Angular, there are some gotchas when using share
and shareReplay
. Observables subscribed in the template with the async
pipe might reach refCount 0
if unsubscribed automatically by the async
pipe when inside a *ngIf
, which would cause a new execution of the source Observable.
You might feel like the God of Time and Cache using share
and shareReplay
, but you should be aware that with great power comes great responsibility. If you want a partner to help you manage the high complexity of share
, shareReplay
and RxJS best practices, contact us today. 🙂
Do you have thoughts?
We’d love to hear them! Join our Community Discord to continue the conversation.