Stop Re-fetching Data: Using RxJS shareReplay for Efficient Caching
Learn how to use RxJS shareReplay to eliminate redundant HTTP requests and implement efficient client-side caching with a focus on the critical refCount configuration.
04 Jun 2026, 22:34 UTC

The Redundant Request Problem
In complex frontend applications, it is common for multiple components to require the same piece of data—such as a user profile or a configuration object—simultaneously. Without a sharing strategy, each component that subscribes to an HTTP request observable triggers its own network call. This results in redundant traffic, wasted bandwidth, and a jittery UI as multiple components resolve the same data at slightly different times.
The solution is shareReplay. This operator transforms a cold observable (which starts a new execution for every subscriber) into a hot observable (which shares a single execution) and caches a specified number of previous emissions for latecomers.
How shareReplay Works
shareReplay performs two primary functions: multicasting and replaying. Multicasting ensures that the source observable is only subscribed to once, regardless of how many downstream observers exist. Replaying uses a buffer to store the most recent values; when a new subscriber joins, shareReplay immediately emits those buffered values before piping in any new emissions from the source.
The Role of refCount
A critical engineering decision when using this operator is the refCount property. This boolean determines how the operator handles the source subscription when the number of active subscribers drops to zero:
- refCount: true: The operator tracks the number of subscribers. When the count hits zero, it unsubscribes from the source. If a new subscriber joins later, the source is restarted.
- refCount: false: The operator maintains the subscription to the source even after all subscribers have left. This is ideal for caching data that should persist for the entire application lifecycle.
Implementation: Caching a Configuration Request
Consider a scenario where an application needs a settings.json file. We want the first component to trigger the fetch, and every subsequent component to receive that same response instantly without another network trip.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { shareReplay, Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class ConfigService {
private config$: Observable<any>;
constructor(private http: HttpClient) {}
getConfig() {
if (!this.config$) {
this.config$ = this.http.get('/api/settings').pipe(
// bufferSize: 1 caches the last emission
// refCount: false ensures the cache persists after components destroy
shareReplay({ bufferSize: 1, refCount: false })
);
}
return this.config$;
}
}
Verification Steps
To verify this implementation, run the application and observe the following in the Browser Developer Tools Network Tab:
- Navigate to a page where the first component calls
getConfig(). You should see one XHR request to/api/settings. - Navigate to a second page or trigger a second component to call
getConfig(). No new network request should appear. - Check the console or a debugger to ensure the second component receives the data immediately upon subscription.
Trade-offs and Memory Risks
While shareReplay is powerful, it introduces specific risks regarding memory management and data freshness.
Memory Leaks
Setting refCount: false creates a permanent subscription to the source. If the source is a long-lived stream (like a WebSocket or a timer) rather than a completing HTTP request, the observable will never be garbage collected, leading to a memory leak. For streams that do not complete, always use refCount: true or combine the stream with a takeUntil operator to force cleanup.
Stale Data
Because shareReplay caches the value, subscribers will always receive the data from the moment the first request was made. If the server-side data changes, the client will not see those changes until the page is refreshed or the cache is manually invalidated. If your data requires real-time accuracy, consider a caching strategy with a TTL (Time To Live) or a mechanism to trigger a refresh by re-assigning the config$ variable to a new observable.
Summary Decision Matrix
| Use Case | refCount | bufferSize | Reasoning |
|---|---|---|---|
| Static Config/Auth User | false | 1 | Fetch once; persist for app lifetime. |
| Shared UI State (Active Tab) | true | 1 | Share while active; cleanup when hidden. |
| Event History/Logs | true | >1 | Provide recent context to new subscribers. |
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.