shareReplay({ refCount }) after upgrading to RxJS 7: old buffered values still replayed to new subscribers
0 reputation · 15 Jun 2025, 08:00 UTC
0 reputation · 15 Jun 2025, 08:00 UTC
We cache an HTTP response with shareReplay so multiple components share one request. After upgrading from RxJS 6 to 7, we want to move from the legacy shareReplay(1) form to the documented configuration object shareReplay({ bufferSize: 1, refCount: true }), mainly so the source subscription is torn down when the last subscriber leaves.
The uncertainty is about what happens to the buffer itself. My understanding is that refCount: true unsubscribes from the source at zero subscribers, but the internal ReplaySubject buffer may not be cleared, so a subscriber arriving later could still receive the previously buffered (now stale) value before any fresh emission. I also understand that once the source errors, the ReplaySubject is terminated and later subscribers receive the cached error rather than triggering a new subscription.
RxJS 7 reworked share() with a configuration object and aligned shareReplay with it, so the reset semantics may differ from what we relied on in 6.x.
refCount: true, is the buffer guaranteed to persist after teardown, or does RxJS 7 reset it?share() config instead?With shareReplay({ bufferSize: 1, refCount: true }) in RxJS 7, the buffer is only discarded when the subscriber count actually falls to zero. If even one subscriber stays alive, the internal ReplaySubject persists, and every later subscriber receives the previously buffered value before any fresh emission. So the stale-replay symptom almost always means your stream never truly reaches zero subscribers — not that RxJS 7 changed the reset semantics against you.
Confirmed for RxJS 7's config-object form:
refCount: true: when the last subscriber unsubscribes, the source subscription is torn down and the next subscription creates a fresh ReplaySubject — the buffer is cleared.refCount: false (default): once started, the source subscription and buffer live indefinitely.Likely explanations for what you're seeing, in order of probability:
takeUntil/async pipe) keeps the count above zero, so the reset never fires.shareReplay(1) had no refCount at all, and teams often paired it with publishReplay(1) + refCount() or the deprecated shareReplay({ refCount: true }) overload — which in 6.x had a known leak where the ReplaySubject was not released. If you mechanically rewrote to the 7.x config object, the reset timing may differ from what the old pipeline actually did.shareReplay instances: if the operator sits inside a factory function that gets re-invoked, each call creates its own buffer, and a leaked instance can outlive its subscribers.RxJS 7's reworked share() accepts resetOnError, resetOnComplete, and resetOnRefCountZero, each taking a boolean or a factory returning an observable (e.g., a timer) that controls when the shared subject is recreated. If you want explicit reset-on-error plus reset-on-refCount-zero semantics, share({ connector: () => new ReplaySubject(1), resetOnError: true, resetOnRefCountZero: true }) gives you the equivalent of shareReplay({ bufferSize: 1, refCount: true }) with error recovery — something shareReplay alone does not offer.
Instrument the source to see whether teardown ever happens:
const shared$ = this.http.get(url).pipe(
tap(() => console.log('source subscribed / re-executed')),
finalize(() => console.log('source torn down')),
shareReplay({ bufferSize: 1, refCount: true })
);If "source torn down" never logs before the next subscriber arrives, the count never hit zero — hunt for the leaked subscription. Also tag emissions with a request id or timestamp to distinguish "replayed old buffer" from "source re-executed but the HTTP layer returned cached data." A quick unit test — subscribe, unsubscribe all, resubscribe, assert the source re-runs — confirms operator behavior independent of your components.
One detail worth checking before changing code: run npm ls rxjs and confirm the exact minor version, since windowTime and reset behavior have subtle version-sensitive edges. If teardown logs correctly and stale values still appear, the most probable culprit is a second shareReplay instance holding its own buffer.
Use comments to ask for clarification. Post a solution as an answer.
29,775 reputation · 15 Jun 2025, 14:52 UTC
To clarify the internal mechanism in RxJS 7: refCount: true manages the subscription to the source, but it does not automatically purge the internal buffer when the subscriber count hits zero.
When a new subscriber arrives after a teardown, the operator re-subscribes to the source, but it immediately emits the values remaining in the ReplaySubject buffer first. This explains why a new subscriber receives stale data before the fresh request is triggered. To ensure a completely clean state upon resubscription, shareReplay may not be the right tool, as its primary purpose is to maintain that buffer across subscription cycles.
You can verify this behavior by using a timer source with shareReplay({ bufferSize: 1, refCount: true }). Subscribe and unsubscribe, wait for the timer to advance, and then re-subscribe; you will see the last emitted value from the previous session delivered immediately before the new timer tick.