Angular HttpClient × RxJS: preventing duplicate requests when multiple subscribers share one call
0 reputation · 12 Feb 2022, 21:47 UTC
0 reputation · 12 Feb 2022, 21:47 UTC
I want one HTTP request to serve several consumers in an Angular application: two async pipe bindings in a template, plus a route resolver that prefetches the same data. The Angular documentation states that HttpClient methods return cold Observables and that no request is sent until subscribe() is called, so each independent subscription issues its own request.
The app is on a current Angular major using provideHttpClient() with standalone components, and RxJS 7 per the lockfile. I am deciding between two documented approaches: multicasting with shareReplay(1) at the service level, or centralizing to a single subscription point (one resolver or one async pipe) and passing data down. I am unsure how shareReplay(1) behaves on error — whether a failed request stays failed for later subscribers — and whether interceptor side effects (auth token attachment, logging) repeat per subscription or per retry attempt.
34775 reputation · 13 Feb 2022, 07:40 UTC
Yes: applying shareReplay(1) (or shareReplay({ bufferSize: 1, refCount: true })) to the HttpClient Observable at the service level is sufficient to guarantee one network request for N concurrent subscribers, including the resolver-plus-template case. The cold HttpClient Observable becomes a shared, replayed stream: the first subscriber (typically the route resolver, which subscribes before the component renders) triggers the single GET, and both async pipe bindings then receive the replayed emission without new requests.
On error, shareReplay does not cache the error permanently in the way people fear, but the nuance matters: when the shared source errors, the error is delivered to current subscribers, and the shared stream resets — the next subscriber triggers a fresh underlying request. So a failed request does not stay failed for later subscribers; a later subscription retries implicitly. If you want explicit control (e.g., don't auto-retry on next subscription), you need your own reset/retry logic.
Interceptors run once per actual HTTP request, not per subscriber of the shared Observable — but they do re-run on every new underlying request, including each retry() resubscription and any post-error resubscription. Write interceptor side effects (token refresh, logging, metrics) to be idempotent.
@Injectable({ providedIn: 'root' })\nexport class UserService {\n private http = inject(HttpClient);\n private users$?: Observable<User[]>;\n\n getUsers(): Observable<User[]> {\n if (!this.users$) {\n this.users$ = this.http.get<User[]>('/api/users').pipe(\n shareReplay({ bufferSize: 1, refCount: true })\n );\n }\n return this.users$;\n }\n\n refresh(): void {\n this.users$ = undefined; // next subscriber triggers a new request\n }\n}The refCount: true option means the underlying subscription is torn down when all observers unsubscribe, which avoids holding the connection alive indefinitely and gives you natural reset behavior when the route is left. If you want the value to survive navigation (true caching), use plain shareReplay(1) and invalidate manually via refresh().
Have the resolver return userService.getUsers() and bind the template with the async pipe against the same service method (or against the resolved data). Because the resolver subscribes first during navigation, the request fires once; the async pipes subscribe later and get the replayed value synchronously.
This assumes RxJS 7 (per your lockfile) and a current Angular major with provideHttpClient(). The error-reset behavior described is the RxJS 7 default for shareReplay; if you rely on specific error semantics, verify against your exact RxJS minor version, since shareReplay configuration defaults changed between RxJS 6 and 7. If consumers subscribe at very different times and you need them all to share one in-flight request but not a stale cached value, consider whether a cache TTL or explicit invalidation fits better than an ever-living replay.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.