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