Mastering RxJS shareReplay: Multicasting, refCount, and UI‑Data Caching
Duplicate HTTP calls in component‑heavy UIs can be eliminated with RxJS shareReplay. Learn how the default refCount in RxJS 7 affects stream lifetime, the trade‑offs of buffer size, and a concrete Angular service example that caches user data efficiently.
07 Aug 2025, 04:44 UTC

Problem: Duplicate HTTP calls in a component‑heavy UI
When a modern web app has many components that need the same data—say a list of users fetched from an API—each component can inadvertently trigger a fresh HTTP request. The side‑effect (network latency, server load, rate limits) is duplicated, and the UI feels sluggish. The question is: how to share a single cold observable so that all subscribers see the same data without re‑executing the side‑effect?
Thesis: shareReplay turns a cold source into a hot, memoized stream and, in RxJS 7, its implicit refCount can be both a blessing and a curse.
RxJS’s shareReplay operator is the go‑to tool for this scenario. It converts a cold observable into a hot one that stores a buffer of past emissions and replays them to new subscribers. In RxJS 7 the operator now defaults to refCount: true, meaning the underlying subscription is automatically torn down when the last observer unsubscribes. That default is convenient for short‑lived streams, but in long‑lived services it can silently kill the shared stream and leave subsequent subscribers with an empty sequence.
Section 1: How shareReplay works under the hood
Consider this cold observable that performs an HTTP GET and logs when it starts:
const source$ = ajax.getJSON('/api/users').pipe(
tap(() => console.log('HTTP request started'))
);
By default, each subscribe call would trigger a new network request. Applying shareReplay changes that:
const shared$ = source$.pipe(shareReplay(1));
The bufferSize of 1 means the last emitted value is cached. Whenever a new subscriber attaches, it receives the cached value immediately if available. The underlying subscription to source$ is created only once, regardless of how many subscribers are attached.
Key properties
- Multicasting: All subscribers share the same execution.
- Replaying: New subscribers get the last
bufferSizevalues. - RefCount (default true): When the last subscriber unsubscribes, the source is unsubscribed automatically.
Section 2: The refCount trade‑off in RxJS 7
In RxJS 6, you had to explicitly pass { refCount: false } to keep the source alive. RxJS 7 changed the default to true to reduce memory leaks in components that mount/unmount frequently. However, this can backfire:
- A component mounts, subscribes, then unmounts after a short interaction.
- The subscription count drops to zero.
- The source is unsubscribed.
- Later, another component subscribes. Because the source was torn down, a new HTTP request is made.
In a UI where data should persist across component lifecycles—such as a global user cache—this behavior is undesirable. The remedy is to disable refCount and manage the subscription yourself.
Example: keep the stream alive with refCount: false
const userCache$ = ajax.getJSON('/api/users').pipe(
shareReplay({ bufferSize: 1, refCount: false })
);
// Manually keep the subscription alive
const keepAlive = userCache$.subscribe();
Now the HTTP request runs once, the data is cached, and any component can tap into userCache$ without triggering a new request. The keepAlive subscription is only unsubscribed when the entire application shuts down.
Section 3: Practical UI data‑fetching pattern
Below is a minimal Angular service that demonstrates the pattern. All components inject UserService and call getUsers() to receive the same cached data.
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { shareReplay } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class UserService {
private users$!: Observable;
constructor(private http: HttpClient) {
this.users$ = this.http.get('/api/users').pipe(
shareReplay({ bufferSize: 1, refCount: false })
);
// Keep the subscription alive for the app’s lifetime
this.users$.subscribe();
}
getUsers(): Observable {
return this.users$;
}
}
Each component can now do:
this.userService.getUsers().subscribe(users => {
this.users = users;
});
Because the underlying HTTP request is shared and cached, the UI updates instantly when the data arrives, and no duplicate requests are made.
Section 4: Trade‑offs and limitations
- Memory usage: The buffer holds the last
bufferSizeemissions. For high‑frequency streams, a large buffer can consume significant memory. ChoosebufferSizecarefully. - Lifecycle management: With
refCount: falseyou must remember to unsubscribe the keep‑alive subscription when the app truly shuts down. Forgetting can lead to memory leaks. - Error handling: If the source errors, the error is replayed to all future subscribers. In some cases you may want to reset the stream after an error;
shareReplaydoes not do that automatically.
Actionable Takeaway
When you need to share a cold observable across a UI and avoid duplicate side‑effects, shareReplay is the right tool. In RxJS 7, always decide whether you want the automatic refCount behavior:
- Use
shareReplay({ bufferSize: 1, refCount: true })for short‑lived streams, such as a single component that mounts/unmounts. - Use
shareReplay({ bufferSize: 1, refCount: false })for long‑lived services or global caches.
Remember to keep the subscription alive if you disable refCount and to monitor buffer size to keep memory usage in check. With these practices, your UI will be efficient, responsive, and free from redundant network traffic.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.