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();
}
}
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Project, ProjectsService } from './projects.service';
@Component({
selector: 'app-project-list',
template: `
<button (click)="refresh()">Refresh</button>
<ul>
<li *ngFor="let p of projects$ | async">
<a [routerLink]="['/projects', p.id]">{{ p.name }}</a>
</li>
</ul>
`
})
export class ProjectListComponent implements OnInit {
projects$!: Observable<Project[]>;
constructor(private projects: ProjectsService) {}
ngOnInit(): void {
this.projects$ = this.projects
.getProjects()
.pipe(map((list) => list.filter((p) => !p.archived)));
}
refresh(): void {
this.projects.invalidate();
this.ngOnInit();
}
}
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { Project, ProjectsService } from './projects.service';
@Component({
selector: 'app-project-detail',
template: `
<section *ngIf="project$ | async as project">
<h2>{{ project.name }}</h2>
<p>Status: {{ project.archived ? 'Archived' : 'Active' }}</p>
</section>
`
})
export class ProjectDetailComponent implements OnInit {
project$!: Observable<Project>;
constructor(
private route: ActivatedRoute,
private projects: ProjectsService
) {}
ngOnInit(): void {
this.project$ = this.route.paramMap.pipe(
switchMap((params) => this.projects.getProject(params.get('id')!))
);
}
}
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
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
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
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
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
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FilterState {
search: string
category: string | null
Zustand for lightweight state management
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
Share this code
Here's the card — post it anywhere.