typescript 113 lines · 3 tabs

Cache and Deduplicate HTTP GET Requests with shareReplay in an Angular Service

Shared by codesnips Sep 2026
3 tabs
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, shareReplay } from 'rxjs/operators';

export interface Project {
  id: string;
  name: string;
  archived: boolean;
}

@Injectable({ providedIn: 'root' })
export class ProjectsService {
  private readonly base = '/api/projects';
  private list$?: Observable<Project[]>;
  private byId = new Map<string, Observable<Project>>();

  constructor(private http: HttpClient) {}

  getProjects(): Observable<Project[]> {
    if (!this.list$) {
      this.list$ = this.http.get<Project[]>(this.base).pipe(
        catchError((err) => {
          this.list$ = undefined;
          return throwError(() => err);
        }),
        shareReplay({ bufferSize: 1, refCount: false })
      );
    }
    return this.list$;
  }

  getProject(id: string): Observable<Project> {
    let stream = this.byId.get(id);
    if (!stream) {
      stream = this.http.get<Project>(`${this.base}/${id}`).pipe(
        catchError((err) => {
          this.byId.delete(id);
          return throwError(() => err);
        }),
        shareReplay({ bufferSize: 1, refCount: false })
      );
      this.byId.set(id, stream);
    }
    return stream;
  }

  invalidate(): void {
    this.list$ = undefined;
    this.byId.clear();
  }
}
3 files · typescript Explain with highlit

This snippet shows the canonical Angular pattern for turning an HTTP GET into a shared, cached stream so that multiple components subscribing to the same data trigger exactly one network call. The core idea is that a cold HttpClient observable fires a fresh request on every subscribe, which is wasteful when several components render the same reference data. Wrapping the request in shareReplay converts it into a multicast, replayable observable: the first subscriber triggers the call, later subscribers receive the last emitted value from the replay buffer, and no duplicate request is made.

In ProjectsService, refCount: false is deliberately chosen so the cache survives even when the subscriber count drops to zero. With the default refCount: true, the underlying request would be torn down once the last component unsubscribes and re-fired for the next one, defeating the cache. The service keeps a Map of in-flight/completed streams keyed by project id in getProject, which both deduplicates concurrent lookups and memoizes results across the app lifetime.

The list$ field caches the collection with shareReplay(1), so any component can subscribe cheaply. A manual invalidate() method drops the cached streams so the next access rebuilds them — important because shareReplay never expires on its own. The catchError inside the pipe removes the failed stream from the cache so a transient error is not replayed forever to future subscribers, a common pitfall with naive caching.

In ProjectListComponent, the template binds directly to projects$ with the async pipe, which handles subscription and teardown automatically. ProjectDetailComponent derives its stream from route params via switchMap into getProject, reusing the service cache. Because both components lean on the same shared observables, navigating between them or rendering them side by side stays cheap.

The trade-off is staleness: cached data can drift from the server until invalidate() runs, so this pattern fits relatively static reference data rather than rapidly changing records. It is a lightweight alternative to a full state-management library when the only requirement is request sharing and simple memoization.


Related snips

typescript
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;

Exponential backoff with jitter for retries

typescript reliability retry
by codesnips 2 tabs
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
javascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["form"]
  static values = { delay: { type: Number, default: 250 } }

Debounced live search with Stimulus + Turbo Streams

rails hotwire stimulus
by codesnips 4 tabs
ruby
json.array! @posts do |post|
  json.cache! ['v1', post], expires_in: 1.hour do
    json.id post.id
    json.title post.title
    json.excerpt post.excerpt
    json.published_at post.published_at

Fragment caching for expensive JSON serialization

rails caching performance
by Alex Kumar 1 tab
typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface FilterState {
  search: string
  category: string | null

Zustand for lightweight state management

react zustand state-management
by Maya Patel 2 tabs
swift
import SwiftUI

struct ContentView: View {
    @State private var username = ""
    @State private var isLoggedIn = false
    @StateObject private var viewModel = LoginViewModel()

SwiftUI declarative UI with state management

swift swiftui ios
by Sofia Martinez 2 tabs

Share this code

Here's the card — post it anywhere.

Cache and Deduplicate HTTP GET Requests with shareReplay in an Angular Service — share card
Link copied