We use cookies to enhance your experience on the site
CodeWorlds

Practical RxJS Patterns in Angular

Search with Debounce

1@Component({
2  template: \`
3    <input #searchInput placeholder="Search for a samurai...">
4    @for (result of searchResults$ | async; track result.id) {
5      <div>{{ result.name }}</div>
6    }
7  \`
8})
9export class SearchComponent implements AfterViewInit {
10  @ViewChild('searchInput') searchInput!: ElementRef;
11  searchResults$!: Observable<Samurai[]>;
12
13  private http = inject(HttpClient);
14
15  ngAfterViewInit(): void {
16    this.searchResults$ = fromEvent<InputEvent>(
17      this.searchInput.nativeElement,
18      'input'
19    ).pipe(
20      map(event => (event.target as HTMLInputElement).value),
21      debounceTime(300),           // Wait 300ms
22      distinctUntilChanged(),       // Ignore the same values
23      filter(query => query.length >= 2),  // Min 2 characters
24      switchMap(query =>            // Cancel previous query
25        this.http.get<Samurai[]>(\`/api/search?q=\${query}\`).pipe(
26          catchError(() => of([]))  // Error handling
27        )
28      )
29    );
30  }
31}

Polling with Retry

1@Injectable({ providedIn: 'root' })
2export class StatusService {
3  private http = inject(HttpClient);
4
5  getStatusWithPolling(): Observable<Status> {
6    return interval(5000).pipe(  // Every 5 seconds
7      startWith(0),              // Immediately on start
8      switchMap(() =>
9        this.http.get<Status>('/api/status').pipe(
10          retry({
11            count: 3,
12            delay: 1000  // Retry 3 times every second
13          }),
14          catchError(error => {
15            console.error('Status check failed:', error);
16            return of({ status: 'unknown', error: true });
17          })
18        )
19      ),
20      shareReplay(1)  // Share result between subscribers
21    );
22  }
23}

Loading with Cache

1@Injectable({ providedIn: 'root' })
2export class CachedDataService {
3  private http = inject(HttpClient);
4  private cache$ = new Map<string, Observable<any>>();
5
6  getData<T>(url: string): Observable<T> {
7    if (!this.cache$.has(url)) {
8      this.cache$.set(url,
9        this.http.get<T>(url).pipe(
10          shareReplay({ bufferSize: 1, refCount: true }),
11          tap({
12            error: () => this.cache$.delete(url)
13          })
14        )
15      );
16    }
17    return this.cache$.get(url)!;
18  }
19
20  invalidateCache(url: string): void {
21    this.cache$.delete(url);
22  }
23}

Handling Multiple Dependent Requests

1getSamuraiWithWeapons(id: number): Observable<SamuraiWithWeapons> {
2  return this.http.get<Samurai>(\`/api/samurai/\${id}\`).pipe(
3    switchMap(samurai =>
4      forkJoin({
5        weapons: this.http.get<Weapon[]>(\`/api/samurai/\${id}/weapons\`),
6        skills: this.http.get<Skill[]>(\`/api/samurai/\${id}/skills\`)
7      }).pipe(
8        map(({ weapons, skills }) => ({
9          ...samurai,
10          weapons,
11          skills
12        }))
13      )
14    )
15  );
16}

Signals vs RxJS - When to Use Which?

| Use Case | Signals | RxJS | |----------|---------|------| | Component state | ✅ | ❌ | | Simple derived calculations | ✅ computed() | ❌ | | HTTP requests | ❌ | ✅ | | Debounce/throttle | ❌ | ✅ | | Multiple transformations | ❌ | ✅ | | Combining streams | ❌ | ✅ |

1// Integration - the best of both worlds
2@Component({...})
3export class HybridComponent {
4  private http = inject(HttpClient);
5
6  // Signal for UI state
7  isLoading = signal(false);
8  selectedId = signal<number | null>(null);
9
10  // RxJS for HTTP
11  samurai$ = toObservable(this.selectedId).pipe(
12    filter(id => id !== null),
13    tap(() => this.isLoading.set(true)),
14    switchMap(id => this.http.get<Samurai>(\`/api/samurai/\${id}\`)),
15    tap(() => this.isLoading.set(false))
16  );
17
18  // Observable → Signal
19  samurai = toSignal(this.samurai$);
20}
Go to CodeWorlds