Always Know When to Use Share vs. ShareReplay

fabioemoutinho

Fábio Englert Moutinho

Posted on July 25, 2022

Always Know When to Use Share vs. ShareReplay

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
*/
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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. 🙂

💖 💪 🙅 🚩
fabioemoutinho
Fábio Englert Moutinho

Posted on July 25, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related