| name | angular-rxjs-patterns |
| user-invocable | false |
| description | Use when handling async operations in Angular applications with observables, operators, and subjects. |
| allowed-tools | ["Bash","Read"] |
Angular RxJS Patterns
Master RxJS in Angular for handling async operations, data streams,
and reactive programming patterns.
Observable Creation
Basic Observable Creation
import { Observable, of, from, interval, fromEvent } from 'rxjs';
const numbers$ = of(1, 2, 3, 4, 5);
const fromArray$ = from([1, 2, 3]);
const fromPromise$ = from(fetch('/api/data'));
const timer$ = interval(1000);
const clicks$ = fromEvent(document, 'click');
const custom$ = new Observable(subscriber => {
subscriber.next(1);
subscriber.next(2);
subscriber.complete();
});
HttpClient Observables
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class DataService {
private readonly http = inject(HttpClient);
getData(): Observable<Data[]> {
return this.http.get<Data[]>('/api/data');
}
getItem(id: string): Observable<Data> {
return this.http.get<Data>(`/api/data/${id}`);
}
createItem(data: Data): Observable<Data> {
return this.http.post<Data>('/api/data', data);
}
updateItem(id: string, data: Data): Observable<Data> {
return this.http.put<Data>(`/api/data/${id}`, data);
}
deleteItem(id: string): Observable<void> {
return this.http.delete<void>(`/api/data/${id}`);
}
}
Common Operators
Transformation Operators
import { map, pluck, switchMap, mergeMap, concatMap } from 'rxjs/operators';
import { of } from 'rxjs';
const numbers$ = of(1, 2, 3).pipe(
map(n => n * 2)
);
const users$ = of(
{ name: 'John', age: 30 },
{ name: 'Jane', age: 25 }
).pipe(
map(user => user.name)
);
searchControl.valueChanges.pipe(
switchMap(term => this.searchService.search(term))
).subscribe(results => {
this.results = results;
});
const ids$ = of(1, 2, 3);
ids$.pipe(
mergeMap(id => this.getUser(id))
).subscribe();
ids$.pipe(
concatMap(id => this.getUser(id))
).subscribe();
Filtering Operators
import { filter, take, takeUntil, takeWhile, distinctUntilChanged } from 'rxjs/operators';
of(1, 2, 3, 4, 5).pipe(
filter(n => n % 2 === 0)
);
interval(1000).pipe(
take(5)
);
const destroy$ = new Subject();
source$.pipe(
takeUntil(destroy$)
).subscribe();
of(1, 1, 2, 2, 3, 3).pipe(
distinctUntilChanged()
);
Combination Operators
import { combineLatest, merge, concat, forkJoin, zip } from 'rxjs';
import { startWith } from 'rxjs/operators';
combineLatest([
this.user$,
this.settings$
]).pipe(
map(([user, settings]) => ({ user, settings }))
).subscribe();
merge(
this.clicks$,
this.hovers$
).subscribe();
concat(
this.loadUser$,
this.loadSettings$
).subscribe();
forkJoin({
user: this.getUser(),
posts: this.getPosts(),
comments: this.getComments()
}).subscribe(({ user, posts, comments }) => {
});
zip(
of(1, 2, 3),
of('a', 'b', 'c')
).pipe(
map(([num, letter]) => `${num}${letter}`)
);
Utility Operators
import { tap, delay, debounceTime, throttleTime, distinctUntilChanged } from 'rxjs/operators';
source$.pipe(
tap(value => console.log('Value:', value)),
map(value => value * 2)
);
of(1, 2, 3).pipe(
delay(1000)
);
searchControl.valueChanges.pipe(
debounceTime(300)
);
clicks$.pipe(
throttleTime(1000)
);
input$.pipe(
distinctUntilChanged()
);
Error Handling
catchError - Handle Errors
import { catchError } from 'rxjs/operators';
import { of, EMPTY, throwError } from 'rxjs';
this.http.get('/api/data').pipe(
catchError(error => {
console.error('Error:', error);
return of([]);
})
);
source$.pipe(
catchError(() => EMPTY)
);
source$.pipe(
catchError(error => {
console.error('Error:', error);
return throwError(() => new Error('Custom error'));
})
);
source$.pipe(
catchError(error => {
if (error.status === 404) {
return of(null);
}
return throwError(() => error);
})
);
retry and retryWhen
import { retry, retryWhen, delay, take } from 'rxjs/operators';
this.http.get('/api/data').pipe(
retry(3)
);
this.http.get('/api/data').pipe(
retryWhen(errors =>
errors.pipe(
delay(1000),
take(3)
)
)
);
this.http.get('/api/data').pipe(
retryWhen(errors =>
errors.pipe(
mergeMap((error, index) => {
if (index >= 3) {
return throwError(() => error);
}
const delayMs = Math.pow(2, index) * 1000;
return of(error).pipe(delay(delayMs));
})
)
)
);
Subscription Management
takeUntilDestroyed (Preferred)
Use takeUntilDestroyed() from @angular/core/rxjs-interop — no ngOnDestroy
needed, and no manual Subject<void> to manage:
import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-my-component',
standalone: true
})
export class MyComponent {
private readonly dataService = inject(DataService);
constructor() {
this.dataService.data$.pipe(
takeUntilDestroyed()
).subscribe(data => {
this.data = data;
});
this.dataService.other$.pipe(
takeUntilDestroyed()
).subscribe(other => {
this.other = other;
});
}
}
DestroyRef for Manual Subscriptions
When subscribing imperatively outside the constructor (e.g. on user action),
use DestroyRef directly:
import { Component, inject } from '@angular/core';
import { DestroyRef } from '@angular/core';
@Component({
selector: 'app-my-component',
standalone: true
})
export class MyComponent {
readonly #destroyRef = inject(DestroyRef);
startPolling() {
const sub = interval(5000).subscribe(() => this.poll());
this.#destroyRef.onDestroy(() => sub.unsubscribe());
}
}
Async Pipe (No Manual Unsubscribe)
import { Component, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { Observable } from 'rxjs';
@Component({
selector: 'app-user-list',
standalone: true,
imports: [AsyncPipe],
template: `
@if (users$ | async; as users) {
@for (user of users; track user.id) {
<div>{{ user.name }}</div>
}
}
@if (loading$ | async) {
<div>Loading...</div>
}
@if (error$ | async; as error) {
<div>Error: {{ error }}</div>
}
`
})
export class UserListComponent {
private readonly userService = inject(UserService);
users$: Observable<User[]> = this.userService.getUsers();
loading$: Observable<boolean> = this.userService.loading$;
error$: Observable<string | null> = this.userService.error$;
}
Subjects
Subject - Multicast
import { Subject } from 'rxjs';
const subject = new Subject<number>();
subject.subscribe(val => console.log('A:', val));
subject.subscribe(val => console.log('B:', val));
subject.next(1);
subject.next(2);
BehaviorSubject - Current Value
import { BehaviorSubject } from 'rxjs';
const subject = new BehaviorSubject<number>(0);
subject.subscribe(val => console.log('A:', val));
subject.next(1);
subject.next(2);
subject.subscribe(val => console.log('B:', val));
@Injectable({
providedIn: 'root'
})
export class StateService {
private stateSubject = new BehaviorSubject<State>(initialState);
state$ = this.stateSubject.asObservable();
updateState(newState: State) {
this.stateSubject.next(newState);
}
get currentState(): State {
return this.stateSubject.value;
}
}
ReplaySubject - Buffer Values
import { ReplaySubject } from 'rxjs';
const subject = new ReplaySubject<number>(2);
subject.next(1);
subject.next(2);
subject.next(3);
subject.subscribe(val => console.log('A:', val));
subject.next(4);
subject.subscribe(val => console.log('B:', val));
AsyncSubject - Last Value on Complete
import { AsyncSubject } from 'rxjs';
const subject = new AsyncSubject<number>();
subject.subscribe(val => console.log('A:', val));
subject.next(1);
subject.next(2);
subject.next(3);
subject.complete();
Hot vs Cold Observables
Cold Observable - Unicast
const cold$ = interval(1000);
cold$.subscribe(val => console.log('A:', val));
setTimeout(() => {
cold$.subscribe(val => console.log('B:', val));
}, 2000);
Hot Observable - Multicast
import { Subject, interval } from 'rxjs';
import { share, shareReplay } from 'rxjs/operators';
const subject = new Subject();
const source$ = interval(1000);
source$.subscribe(subject);
subject.subscribe(val => console.log('A:', val));
setTimeout(() => {
subject.subscribe(val => console.log('B:', val));
}, 2000);
const shared$ = interval(1000).pipe(share());
shared$.subscribe(val => console.log('A:', val));
setTimeout(() => {
shared$.subscribe(val => console.log('B:', val));
}, 2000);
const cached$ = this.http.get('/api/data').pipe(
shareReplay(1)
);
cached$.subscribe();
cached$.subscribe();
RxJS in Services
Data Service with State
Use signals for synchronous state. Use observables for async HTTP operations,
converting them to signals with toSignal() when needed in templates:
import { Injectable, inject, signal, computed } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, catchError, finalize, tap } from 'rxjs';
import { of } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class UserService {
private readonly http = inject(HttpClient);
readonly users = signal<User[]>([]);
readonly loading = signal(false);
readonly error = signal<string | null>(null);
readonly hasUsers = computed(() => this.users().length > 0);
loadUsers(): void {
this.loading.set(true);
this.error.set(null);
this.http.get<User[]>('/api/users').pipe(
tap(users => this.users.set(users)),
catchError(err => {
this.error.set(err.message);
return of([]);
}),
finalize(() => this.loading.set(false))
).subscribe();
}
getUser(id: string): Observable<User> {
return this.http.get<User>(`/api/users/${id}`);
}
}
Template with signals (no async pipe needed for signal state):
@Component({
selector: 'app-user-list',
standalone: true,
template: `
@if (userService.loading()) {
<div>Loading...</div>
}
@if (userService.error(); as error) {
<div>Error: {{ error }}</div>
}
@for (user of userService.users(); track user.id) {
<div>{{ user.name }}</div>
}
`
})
export class UserListComponent {
protected readonly userService = inject(UserService);
constructor() {
this.userService.loadUsers();
}
}
Search Service with Debounce
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, Subject, of } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class SearchService {
private readonly http = inject(HttpClient);
private readonly searchTerms = new Subject<string>();
readonly results$: Observable<SearchResult[]> = this.searchTerms.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.search(term))
);
search(term: string): Observable<SearchResult[]> {
if (!term.trim()) {
return of([]);
}
return this.http.get<SearchResult[]>(`/api/search?q=${term}`);
}
setSearchTerm(term: string): void {
this.searchTerms.next(term);
}
}
Testing RxJS
Testing Observables
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [UserService]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should fetch users', () => {
const mockUsers = [{ id: 1, name: 'John' }];
service.getUsers().subscribe(users => {
expect(users).toEqual(mockUsers);
});
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
});
Testing with Marble Diagrams
import { TestScheduler } from 'rxjs/testing';
describe('Marble tests', () => {
let scheduler: TestScheduler;
beforeEach(() => {
scheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
});
it('should debounce', () => {
scheduler.run(({ cold, expectObservable }) => {
const source$ = cold('-a-b-c|');
const expected = '-----c|';
const result$ = source$.pipe(debounceTime(20));
expectObservable(result$).toBe(expected);
});
});
});
When to Use This Skill
Use angular-rxjs-patterns when building modern, production-ready
applications that require:
- Complex async data flows
- Real-time updates and streaming data
- Efficient HTTP request management
- Form input handling with debouncing
- State management with observables
- Error handling and retry logic
- Combining multiple async sources
- Memory-safe subscription management
RxJS Best Practices in Angular
- Use
takeUntilDestroyed() - Automatic cleanup without ngOnDestroy
- Use signals for state -
BehaviorSubject → signal() for synchronous state
- Use async pipe for observables in templates - Automatic subscription management
- Use
@if/@for control flow - Replaces *ngIf/*ngFor
- Use
inject() - Cleaner than constructor injection
- shareReplay for caching - Avoid duplicate HTTP requests
- debounceTime for inputs - Reduce API calls
- switchMap for cancellation - Cancel old requests
- catchError for errors - Always handle errors
- Test observables properly - Use marble diagrams
Common RxJS Mistakes
- Not unsubscribing - Memory leaks
- Nested subscriptions - Callback hell
- Not using operators - Imperative instead of declarative
- Subscribing in services - Return observables instead
- Not handling errors - Silent failures
- Using Subject incorrectly - Prefer BehaviorSubject for state
- Not using shareReplay - Duplicate HTTP requests
- Forgetting to complete subjects - Memory leaks
- Using subscribe in templates - Use async pipe
- Not understanding hot vs cold - Unexpected behavior
Resources