Share / ShareReplay / RefCount
Tags: rxjs, angular
Published: 2023-08-01 11:32:07

`share` and `ShareReplay` are two RxJs operators that we always struggle to use correctly. We know that we can reach for them when we want to multicast a costly observable or cache a value that will be used at multiple places. But what are the key differences between both, and what is the `refCount` flag and how can we leverage its behavior?

In this article, I will try to explain them for you so that you will not need to ask this question again.

---

## Example 1
I posted the following question on Twitter which unfortunately received zero responses. This really highlights the lack of understanding regarding `share` and `shareReplay`.

{% embed https://twitter.com/laforge_toma/status/1683193488060416000?s=20 %}

The exercice looks like this:

```ts
@Component({
  selector: 'app-count',
  standalone: true,
  imports: [NgIf, AsyncPipe],
  template: `
    <ng-container *ngIf="flag"> {{ count1$ | async }} </ng-container>
    <ng-container *ngIf="!flag"> {{ count2$ | async }} </ng-container>
  `,
})
export class CountComponent implements OnInit {
  flag = true;

  readonly count$ = interval(1000).pipe(
    take(7),
    shareReplay({ bufferSize: 1, refCount: false }) // 👈 line: 15
  );

  readonly count1$ = this.count$.pipe(
    take(3),
    map((c) => `count1: ${c}`)
  );
  readonly count2$ = this.count$.pipe(
    take(3),
    map((c) => `count2: ${c}`)
  );

  ngOnInit(): void {
    setTimeout(() => {
      this.flag = false;
    }, 5500);
  }
}
```
**Note:** This code snippet is written using Angular, but the behavior of both operator is the same outside the framework.

When the `flag` is true, we subscribe to `count1$` and after 3 emissions, the observable completes. Then after 5500ms, the `flag` is set to false and we subscribe to `count2$` which also completes after 3 emissions. Both observables are chained with `count$`.

The goal is to predict the result displayed on the screen after 10s, depending on the operator used in line 15.

### Share
Let's start with the `share` operator. 
The `share` operator will multicast each value emitted by the source observable, which means we won't re-execute the source observable for each new subscription.

Moreover, when the **count of subscribers drops to 0, the source observable will be unsubscribed**.

Inside this operator, **we use a `Subject` as a connector** between the source observable and the subscribers. This means that **every late subscriber will NOT have access to the previously emitted data**.

You should use this operator if you know that you will not use previously emitted data and are only interested in upcoming ones.

#### Solution
If we go back to our exemple:
1. `count$` will be triggered when `count1$` subscribes to it. 
2. After `count$` emits 3 values, `count1$` completes due to the `take(3)` operator. Consequently `count$` will also complete since the number of subscribers drops to 0 and the inner Subject will reset.
3. After 5500ms `count2$` starts. It will subscribe to `count$` and `count$` will start emitting from the beginning. 
4. Since we have a take(3) on the observable, **the final answer is 3.**

### ShareReplay with refCount: true
Both `share` and `shareReplay` operators behave almost the same:  `ShareReplay` use `share` under the hood. The crucial difference lies in the connector: **shareReplay uses a `ReplaySubject` instead of a Subject**. This distinction becomes significant when dealing with late subscribers, as they will have access to previously emitted data.

Another difference is the ability to toggle the `refCount` flag. When `refCount=true`, it allows unsuscribing from the source observable when the subscriber count drops to 0. The `share` operator's `refCount` flag is defaulted to true.

In this scenario with `refCount` set to true, the source observable will get unsubscribed when the subscriber count drops to 0.

#### Solution
If we go back to our exemple:
1. `count$` will be triggered when `count1$` subscribes to it.
2. After `count$` emits 3 times, `count1$` completes due to the `take(3)` operator. Consequently `count$` will also complete since the number of subscribers drops to 0 and the `refCount` flag is set to true.
3. After 5500ms `count2$` starts, it will subscribe to `count$` again and `count$` will start emitting from the beginning.
4. Since we have a `take(3)` on the observable, **the final answer is 3.** In this example, both `share` and `shareReplay` bahave exactly the same. However, we will see more examples below to understand the differences between them.

### ShareReplay with refCount: false
As explained above, **setting the `refCount` flag to false will keep the source observable alive even if the subscriber count drops to 0.**

This is dangerous because **if the source observable never completes, this can create memory leaks.**

However in some cases, you may not want to re-execute the source observable if a new subscriber subscribes, such as in the case of an HTTP request.

#### Solution
If we go back to our exemple:
1. `count$` will be triggered when `count1$` subscribes to it.
2. After `count$` emits 3 times, `count1$` completes due to the `take(3)` operator, BUT `count$` will NOT complete and continue to emit a value every 1s.
3. After 5500ms `count2$` starts, it will subscribe to `count$` and receive the last emitted value which is 4.
4. Since we have a `take(3)` on the observable, **the final answer is 6.**

**Note:** Since all observables completes, we don't have any memory leaks issues

## Example 2
Let's take another example to better understand the difference between `share` and `shareReplay`. This distinction becomes more evident when we apply the operators to an observable that never completes, such as a `BehaviorSubject`.

```ts
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [NgIf, AsyncPipe],
  template: `
    <ng-container *ngIf="!flagFinalize">
      <ng-container> {{ count1$ | async }} </ng-container>
      <ng-container *ngIf="flag"> {{ count2$ | async }} </ng-container>
    </ng-container>
    <button (click)="flagFinalize = !flagFinalize">FINALIZE</button>
    <button (click)="subject.next(subject.value + 1)">INCREMENT</button>
  `,
})
export class AppComponent implements OnInit {
  flag = false;
  flagFinalize = false;

  subject = new BehaviorSubject(0);

  readonly count$ = this.subject.pipe(
    tap({
      next: (t) => console.log('I get next value of count', t),
      complete: () => console.log('complete count'),
      finalize: () => console.log('finalize count'),
    }),
    share() // 👈
  );

  readonly count1$ = this.count$.pipe(
    tap({
      next: (t) => console.log('I get next value of count1', t),
      complete: () => console.log('complete count1'),
      finalize: () => console.log('finalize count1'),
    }),
    map((c) => `count1: ${c}`)
  );
  readonly count2$ = this.count$.pipe(
    tap({
      next: (t) => console.log('I get next value count2', t),
      complete: () => console.log('complete count2'),
      finalize: () => console.log('finalize count2'),
    }),
    map((c) => `count2: ${c}`)
  );

  ngOnInit(): void {
    setTimeout(() => {
      this.flag = true;
    }, 1000);
  }
}
```
This time we are using a `BehaviorSubject` and we have an INCREMENT button to emit a  value to the `Subject`.

Additionally, we have added a `tap` operator to log the `next`, `complete` and `finalize` events.

The FINALIZE button allows us to unsubscribe from the `count1$` and `count2$` observables.

### Scenario
When the app is loaded, `count1$` subscribes to `count$`, and after 1s `count2$` also subscribes to `count1$`.

Next, we click once on the INCREMENT button, and finally we click on the FINALIZE button.
 
Before reading the article further, I invite you to try to guess what the behavior of this scenario will be with each operator. Once you are done, compare your ideas with the solution.

### Share
1. When `count1$` subscribes to `count$`, the `count$` observable will start emitting values and `count1$` will receive the initial value 0.
2. In this scenario, `count1$` doesn't complete so the number of subscribers will not drop to 0, and `count$` will not complete either.
3. After 1s, `count2$` starts subscribing to `count$`, but since we are using the share operator, the inner observable is a `Subject` which will not replay the previous emitted value. Therefore `count2$` will not receive any value.
4. We now click on the INCREMENT button, and both `count1$` and `count2$ `will be notified with the value 1.
5. Finally we click on the FINALIZE button, and both `count1$` and `count2$` will get unsubscribed (_due to the `asyncPipe`_) and finalized. Since the `share` operator unsubscribes the source when all subscribers drop to 0, `count$` will finalize as well.

### ShareReplay with refCount: true 
We replace the `share` operator with `shareReplay({bufferSize: 1, refCount: true})`

1. same behavior as previously
2. same thing
3. After 1s, `count2$` starts subscribing to `count$`. However this time, `count2$` gets the previous emitted value (0 in our case), because `shareReplay` uses a `ReplaySubject` as the connector. This is the significant difference between both operators

4 and 5 are identical since the `refCount` flag is set to true.

### ShareReplay with refCount: false
1 to 4 is identical to the previous scenario, and all differences are seen in point 5 when we unsubscribe from the subscribers.

5. When we click on the FINALIZE button, `count1$` and `count2$` will be unsubscribed correctly due to the `asyncPipe`. However `count$` will not finalize because `shareReplay` will not unsubscribe the inner source and `count$` will continue to exist indefinitely, which might cause a memory leak.

**Important note:** If you have noticed, I said that this might cause a memory leak. If you lose the reference to your running observable, such as when destroying a component, a new `count$` observable will be instantiated next time you initialize the particular component. This can lead to memory leaks.

In the above example, switching back the flag, `count1$` and `count2$` will resubscribe to the existing observable. `ShareReplay` with `refCount` set to false is useful when you don't want to re-execute a costly observable like an HTTP request. You can set your shared observable in a global service and inject it anywhere in your application. The instance will never be unsubscribed, but when new subscribers subscribe to the observable, they will use the existing instance.

**Note:** We often see the shorthand synthax `replaySubject(1)`, which is the shorthand for `replaySubject({bufferSize: 1, refCount: false})`. So you need to be careful about using this synthax. You will more often reach for `refCount` set to true to avoid memory leak issues.

## Example 3
In the last example, we will use a source observable that completes, similar to an HTTP request. To simplify the example, we will use the `of` operator.

```ts
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [NgIf, AsyncPipe],
  template: `
    <ng-container> {{ request1$ | async }} </ng-container>
    <ng-container *ngIf="flag"> {{ request2$ | async }} </ng-container>
  `,
})
export class AppComponent implements OnInit {
  flag = false;

  readonly http$ = of('trigger http request').pipe(
    tap({
      next: (t) => console.log('http response', t),
      complete: () => console.log('complete http'),
      finalize: () => console.log('finalize http'),
    }),
    share() // 👈
  );

  readonly request1$ = this.http$.pipe(
    tap({
      next: (t) => console.log('request1 response', t),
      complete: () => console.log('complete request1'),
      finalize: () => console.log('finalize request1'),
    }),
    map((c) => `request1: ${c}`)
  );
  readonly request2$ = this.http$.pipe(
    tap({
      next: (t) => console.log('request2 response', t),
      complete: () => console.log('complete request2'),
      finalize: () => console.log('finalize request2'),
    }),
    map((c) => `request2: ${c}`)
  );

  ngOnInit(): void {
    setTimeout(() => {
      this.flag = true;
    }, 1000);
  }
}

```

### Scenario
When we load the component, we trigger a first HTTP request using the observable `http$`. After 1s, we want to get the result of the same request. To achieve this, we consider using either `share` or `shareReplay` operator to cache the result.

Same exercice as previously, I encourage you to think first before reading the solution below.

### Share
1. `request1$` subscribes to `http$` which triggers an HTTP call. Once the response comes back, the HTTP call completes, causing `http$` and `request1$` to complete as well.
2. After 1s, `request2$` subscribes to `http$` hoping to get the result of the same HTTP call. However, since `share` uses a `Subject` under the hood, nothing is cached, and `http$` is re-executed, resulting in a new HTTP call.

### ShareReplay
In this scenario, the `refCount` doesn't change the behavior since the source observable (`http$`) completes on its own, irrespective of the number of subscribers.

1. same as previously
2. After 1s, `request2$` subscribes to `http$` but this time, `shareReplay` uses a `ReplaySubject` as the connector, allowing it to store the last value emitted by `http$`. Therefore `http$` doesn't need to be re-executed, and `request2$` received the cached value without triggering a new HTTP call.

**Note:** Be careful when using `shareReplay` inside a **global service** behind a http call. Each new subscriber will receive the cached value and the HTTP request will NEVER fire again. As a result, your data will NEVER get refreshed.

## Conclusion
In summary, `shareReplay` is useful in scenarios where you want to cache and replay the last emitted value of an observable, especially in situations like HTTP requests, to avoid unnecessary re-execution and improve performance. **But be careful, this is useful inside a component scope, generally not within the global scope.**

You need to think carefully about the `refCount` flag when using `shareReplay` on observables that don't complete on their own.

`share` is useful when you want to multicast a long-living observable and you don't need to access previously emitted data.

---

As you can see, understanding exactly how this two operators work under the hood can help you improve your application's performance.

---

I hope you now have a better understanding of the differences between `share` and `shareReplay` and the importance of the `refCount` flag. With this knowledge, you should be able to use them correctly and truly understand what is happening behind the scenes.

---

You can find me on [Twitter](https://twitter.com/laforge_toma) or [Github](https://github.com/tomalaforge).Don't hesitate to reach out to me if you have any questions.