DEV Community

Cover image for Share / ShareReplay / RefCount
thomas for This is Angular

Posted on • Edited on • Originally published at Medium

Share / ShareReplay / RefCount

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.

The exercice looks like this:

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

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.

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

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.

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

@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);
  }
}

Enter fullscreen mode Exit fullscreen mode

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 or Github.Don't hesitate to reach out to me if you have any questions.

Top comments (2)

Collapse
 
divnych profile image
divnych

Code pieces you come up with in your articles are so sweat. Thanks a lot.

Collapse
 
ngnam profile image
NamNguyen

thanks pro