shareReplay
Imagine you have an Observable that does some expensive work when someone subscribes (like making an HTTP request). If multiple parts of your application subscribe to this same Observable independently, the expensive work will happen multiple times (multiple identical HTTP requests!).
shareReplay
solves this by:
- Sharing a Single Subscription: It ensures that only one subscription is ever made to the original source Observable, no matter how many downstream subscribers there are.
- Multicasting Results: It takes the values emitted by that single source subscription and broadcasts them to all downstream subscribers.
- Replaying Buffered Values: It keeps a buffer of the most recent values (you specify how many) and immediately sends those buffered values to any new subscriber that joins later.
Analogy#
Think of watching a live stream on the internet that also has DVR/replay capabilities.
- The Original Broadcast (Source Observable): The actual live event happening once.
- The Streaming Service (
shareReplay
): It takes the single live broadcast. - Viewers (Subscribers): People tuning in to watch.
- The first viewer causes the streaming service to connect to the original broadcast.
- All viewers watch the same broadcast via the streaming service (multicasting).
- Someone tuning in late can immediately see the last few minutes (replaying the buffer) before catching up to the live feed.
- The streaming service only needs one connection to the original broadcast source, regardless of how many viewers there are.
Key Configuration#
shareReplay
is typically configured with an object: shareReplay({ bufferSize: 1, refCount: true })
bufferSize
: How many of the latest emissions to buffer and replay to new subscribers.bufferSize: 1
is very common, especially for HTTP requests where you just want the single result cached and shared.refCount
: (Reference Counting) This is crucial!refCount: true
: The operator keeps track of how many active subscribers there are. It subscribes to the source Observable only when the first subscriber arrives. It unsubscribes from the source Observable when the last subscriber unsubscribes. This is usually what you want for things like HTTP requests to avoid keeping connections or resources active unnecessarily. If a new subscriber arrives later, it will re-subscribe to the source.refCount: false
: The subscription to the source Observable, once established by the first subscriber, stays active forever (or until the source completes/errors), even if all subscribers leave. Use this only if you intend for the source to keep running in the background regardless of subscribers.
Real-World Example: Efficiently Fetching Shared Configuration Data#
Imagine multiple components in your application need access to some configuration data fetched from an API endpoint (/api/config
). Without shareReplay
, each component subscribing to the fetch operation would trigger a separate HTTP GET request.
Code Snippets:
1. Configuration Service (config.service.ts
)
import { Injectable, inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable, shareReplay, tap, timer, switchMap } from "rxjs";
export interface AppConfig {
apiUrl: string;
featureFlags: {
newDashboard: boolean;
betaTesting: boolean;
};
theme: string;
}
@Injectable({
providedIn: "root",
})
export class ConfigService {
private http = inject(HttpClient);
private configUrl = "/api/app-config"; // Your API endpoint
// --- Shared Config Observable ---
// This is the Observable that components will subscribe to.
readonly config$: Observable<AppConfig>;
constructor() {
// Make the HTTP request ONLY ONCE and share the result.
this.config$ = this.http.get<AppConfig>(this.configUrl).pipe(
tap(() =>
console.log(
"%c Fetching application config from API... ",
"background: #ffcc00; color: black;"
)
),
// --- Key Operator ---
shareReplay({
bufferSize: 1, // Cache and replay the single config object
refCount: true, // Unsubscribe from HTTP when no components are listening
})
// --------------------
);
// Example of a source that emits periodically - shareReplay works here too
// this.config$ = timer(0, 5000).pipe( // Emit every 5 seconds
// switchMap(() => this.http.get<AppConfig>(this.configUrl)),
// tap(() => console.log('%c Fetching application config from API... ', 'background: #ffcc00; color: black;')),
// shareReplay({ bufferSize: 1, refCount: true })
// );
}
// You might still have methods for specific actions, but data access is via config$
}
2. Component A - Consumes Config
import {
Component,
inject,
signal,
OnInit,
DestroyRef,
ChangeDetectionStrategy,
} from "@angular/core";
import { CommonModule } from "@angular/common";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ConfigService, AppConfig } from "./config.service"; // Adjust path
@Component({
selector: "app-comp-a",
standalone: true,
imports: [CommonModule],
template: `
<div class="component-box">
<h4>Component A</h4>
@if (config()) {
<p>API URL: {{ config()?.apiUrl }}</p>
<p>Theme: {{ config()?.theme }}</p>
} @else {
<p>Loading config...</p>
}
</div>
`,
styles: [
".component-box { border: 1px solid blue; padding: 10px; margin: 10px; }",
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CompAComponent implements OnInit {
private configService = inject(ConfigService);
private destroyRef = inject(DestroyRef);
config = signal<AppConfig | null>(null);
ngOnInit(): void {
console.log("CompA: Subscribing to config$");
this.configService.config$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((cfg) => {
console.log("CompA: Received config", cfg);
this.config.set(cfg);
});
}
}
3. Component B - Consumes Config
import {
Component,
inject,
signal,
OnInit,
DestroyRef,
ChangeDetectionStrategy,
} from "@angular/core";
import { CommonModule } from "@angular/common";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ConfigService, AppConfig } from "./config.service"; // Adjust path
@Component({
selector: "app-comp-b",
standalone: true,
imports: [CommonModule],
template: `
<div class="component-box" style="border-color: green;">
<h4>Component B</h4>
@if (config()) {
<p>
New Dashboard Feature:
{{ config()?.featureFlags?.newDashboard ? "ENABLED" : "DISABLED" }}
</p>
<p>
Beta Testing: {{ config()?.featureFlags?.betaTesting ? "ON" : "OFF" }}
</p>
} @else {
<p>Loading config...</p>
}
</div>
`,
styles: [
".component-box { border: 1px solid blue; padding: 10px; margin: 10px; }",
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CompBComponent implements OnInit {
private configService = inject(ConfigService);
private destroyRef = inject(DestroyRef);
config = signal<AppConfig | null>(null);
ngOnInit(): void {
console.log("CompB: Subscribing to config$");
// Simulate CompB loading slightly later
setTimeout(() => {
this.configService.config$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((cfg) => {
console.log("CompB: Received config", cfg);
this.config.set(cfg);
});
}, 50); // Simulate slight delay
}
}
4. App Component
import { Component } from "@angular/core";
import { CompAComponent } from "./comp-a.component"; // Adjust path
import { CompBComponent } from "./comp-b.component"; // Adjust path
@Component({
selector: "app-root",
standalone: true,
imports: [CompAComponent, CompBComponent], // Import components
template: `
<h1>RxJS shareReplay Demo</h1>
<app-comp-a></app-comp-a>
<app-comp-b></app-comp-b>
`,
})
export class AppComponent {}
Explanation:
- The
ConfigService
definesconfig$
. Inside the constructor, it chainshttp.get(...)
withtap()
(for logging the fetch attempt) and thenshareReplay({ bufferSize: 1, refCount: true })
. - When
CompAComponent
initializes (ngOnInit
), it subscribes toconfigService.config$
. This is the first subscription. - Because it's the first subscription and
refCount
is true,shareReplay
subscribes to its source (thehttp.get
). The HTTP request is made. You'll see the "Fetching application config from API..." log message once. - When the HTTP request completes,
shareReplay
receives theAppConfig
data. It buffers this single value (bufferSize: 1
) and sends it toCompAComponent
. - A moment later,
CompBComponent
initializes and subscribes to the sameconfigService.config$
. - Because
shareReplay
already has an active source subscription and a buffered value, it does not trigger a new HTTP request. Instead, it immediately replays the bufferedAppConfig
value toCompBComponent
. You will not see the "Fetching..." log message a second time. - Both components now have the same configuration data, fetched with only a single API call.
- If both
CompA
andCompB
were destroyed (causing their subscriptions viatakeUntilDestroyed
to end),shareReplay
(becauserefCount: true
) would notice the subscriber count dropped to zero and would unsubscribe from the sourcehttp.get
Observable. If a new component subscribed later, the fetch process would start again.
Summary#
shareReplay
is essential for optimizing applications by preventing redundant work and ensuring multiple parts of your UI react to the same shared data stream efficiently.