RxJs: share vs shareReplay

There is often the need to share the emissions of an Observable with multiple observers, usually to cache taxing computations or HTTP requests. That can be achieved with share or shareReplay, and now you wonder, which one is best for your use case? This article describes their behavior so you can make the best choice.

share

share is used to multicast the values of an Observable, which means it will share the values emitted by the source Observable to multiple observers.

const source$ = interval(1000).pipe(
  take(3),
  map(() => Math.floor(Math.random() * 10)),
  tap({
    subscribe: () => console.log('Subscribed to source'),
    next: (x) => console.log('Source: ', x),
    complete: () => console.log('Source completed'),
  })
);

const shared$ = source$.pipe(share());

shared$.subscribe((x) => console.log('subscription 1: ', x));
shared$.subscribe((x) => console.log('subscription 2: ', x));

// Output:
// Subscribed to source

// Source: 6
// subscription 1: 6
// subscription 2: 6

// Source: 4
// subscription 1: 4
// subscription 2: 4

// Source: 7
// subscription 1: 7
// subscription 2: 7

// Source completed

In the example above it can be seen that the subscription to the source is happening once, and the observers share the emitted values, that is because internally share subscribes to the source Observable and uses a Subject to emit the values received from the source to all observers.

Another important aspect of share is that it keeps a count of subscribers, when that number drops to 0 it resets the source Observable, meaning it will unsubscribe from it, and the following subscriber by subscribing will trigger a new execution of the source.

const source$ = interval(1000).pipe(
  map(() => Math.floor(Math.random() * 10)),
  tap({
    subscribe: () => console.log('Subscribed to source'),
    next: (x) => console.log('Source: ', x),
    unsubscribe: () => console.log('Unsubscribed from source'),
  }),
  take(6)
);

const shared$ = source$.pipe(share(), take(3));

shared$.subscribe((x) => console.log('subscription 1: ', x));
shared$.subscribe((x) => console.log('subscription 2: ', x));

setTimeout(
  () => shared$.subscribe((x) => console.log('subscription 3: ', x)),
  2500
);

setTimeout(
  () => shared$.subscribe((x) => console.log('subscription 4: ', x)),
  6000
);

// Output:
// Subscribed to source

// (~1000ms)
// Source: 3
// subscription 1: 3
// subscription 2: 3

// (~2000ms)
// Source: 0
// subscription 1: 0
// subscription 2: 0

// (~3000ms)
// Source: 1
// subscription 1: 1
// subscription 2: 1
// subscription 3: 1

// (~4000ms)
// Source: 4
// subscription 3: 4

// (~5000ms)
// Source: 8
// subscription 3: 8
// Unsubscribed from source

// (~6000ms)
// Subscribed to source

// (~7000ms)
// Source: 7
// subscription 4: 7

// (~8000ms)
// Source: 8
// subscription 4: 8

// (~9000ms)
// Source: 3
// subscription 4: 3
// Unsubscribed from source

In the example above, since subscriber 4 arrives after the other three subscribers were completed, the source was reset(resubscribed) and subscriber 4 received a new set of values.

share is an operator which converts a unicast Observable to a multicast one, or in other words, converts a cold Observable into a hot one. Learn more about cold & hot, and other pitfalls of share in my other article.

shareReplay

shareReplay is like share with a slight difference, it can replay a specified number of previous emissions to new subscribers. Replaying previous values is useful in scenarios where you have late subscribers and want to cache the previous emissions, especially useful if those values are the result of taxing computations.

const source$ = interval(1000).pipe(
  take(4),
  tap({
    subscribe: () => console.log('Subscribed to source'),
    next: (x) => console.log('Source: ', x),
    complete: () => console.log('Source completed'),
    unsubscribe: () => console.log('Unsubscribed from source'),
  })
)

const shared$ = source$.pipe(
  shareReplay(2),
  take(3)
);

shared$.subscribe(x => console.log('subscriber 1: ', x));
shared$.subscribe(y => console.log('subscriber 2: ', y));

setTimeout(() => {
  shared$.subscribe(y => console.log('subscriber 3: ', y));
}, 6000);

// Output:
// Subscribed to source

// (~1000ms)
// Source: 0
// subscriber 1: 0
// subscriber 2: 0

// (~2000ms)
// Source: 1
// subscriber 1: 1
// subscriber 2: 1

// (~3000ms)
// Source: 2
// subscriber 1: 2
// subscriber 2: 2

// (~4000ms)
// Source: 3
// Source completed

// (~6000ms)
// subscriber 3: 2
// subscriber 3: 3

The above example describes well how shareReplay works, now to explain what and how is it different from share

We know that share uses a Subject to multicast, but shareReplay also replies previous emissions so it must use something else, right? Right, it uses the ReplaySubject which is like a Subject but can also reply a specified number of emissions.

When subscriber 3 arrived later, it got the last two emissions of the source (2 and 3).

Another thing you can observe with the 4th emission of the source is that shareReplay by default does not keep a count of subscribers, so it does not unsubscribe from the source when the count reaches 0, making the source emit values even if there are no observers to listen to it. Count of subscribers (refCount) can be enabled if needed.

const source$ = interval(1000).pipe(
  take(4),
  tap({
    subscribe: () => console.log('Subscribed to source'),
    next: (x) => console.log('Source: ', x),
    complete: () => console.log('Source completed'),
    unsubscribe: () => console.log('Unsubscribed from source'),
  })
);

const shared$ = source$.pipe(
  shareReplay({ bufferSize: 2, refCount: true }),
  take(3)
);

shared$.subscribe((x) => console.log('subscriber 1: ', x));

setTimeout(() => {
  shared$.subscribe((y) => console.log('subscriber 2: ', y));
}, 2500);

setTimeout(() => {
  shared$.subscribe((y) => console.log('subscriber 3: ', y));
}, 6000);

// Output:
// Subscribed to source

// (~1000ms)
// Source: 0
// subscriber 1: 0

// (~2000ms)
// Source: 1
// subscriber 1: 1

// (~2500ms)
// subscriber 2: 0
// subscriber 2: 1

// (~3000ms)
// Source: 2
// subscriber 1: 2
// subscriber 2: 2
// Unsubscribed from source

// (~6000ms)
// Subscribed to source

// (~7000ms)
// Source: 0
// subscriber 3: 0

// (~8000ms)
// Source: 1
// subscriber 3: 1

// (~9000ms)
// Source: 2
// subscriber 3: 2
// Unsubscribed from source

With refCount set to true when the subscribers count drops to 0 like with share, it unsubscribes from the source Observable and the next Observer on subscription triggers a new execution of the source. And because it unsubscribes when there are no subscribers, the source observable will not emit values anymore (Source never emitted 3).