install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/angular-rxjs-patterns" ~/.claude/skills/intense-visions-harness-engineering-angular-rxjs-patterns && rm -rf "$T"
manifest:
agents/skills/claude-code/angular-rxjs-patterns/SKILL.mdsource content
Angular RxJS Patterns
Apply RxJS patterns correctly in Angular — switchMap for HTTP, takeUntilDestroyed for cleanup, async pipe for templates, and catchError for resilience
When to Use
- Fetching data in response to route params, search input, or user events
- Managing multiple concurrent HTTP requests (cancel-on-change, parallel, sequential)
- Cleaning up subscriptions when a component or service is destroyed
- Sharing a single HTTP response across multiple subscribers
- Handling errors from observables without breaking the stream
Instructions
- Always unsubscribe from observables in components. Use
(Angular 16+) instead oftakeUntilDestroyed(this.destroyRef)
+ngOnDestroy
teardown patterns.Subject - Use
when a new event should cancel the previous in-flight request (e.g., search typeahead). UseswitchMap
when order matters and requests must not overlap. UseconcatMap
when all concurrent requests are independent.mergeMap - Use the
pipe in templates instead of manual subscriptions in the component class. It handles subscribe, unsubscribe, and change detection automatically.async - Share expensive observables (HTTP calls) with
to prevent duplicate requests when multiple consumers subscribe.shareReplay(1) - Handle errors with
inside acatchError
chain. Returnpipe()
to recover, orof(fallbackValue)
to propagate. Never swallow errors silently.throwError(() => err) - Use
for state that needs an initial value and synchronous read (BehaviorSubject
). Expose only the observable side via.value
— keepasObservable()
private to the service..next() - Avoid nested subscriptions (
insidesubscribe()
). Flatten withsubscribe()
,switchMap
, ormergeMap
.combineLatest - Debounce user input with
before triggering HTTP requests. Pair withdebounceTime(300)
to skip identical values.distinctUntilChanged()
@Injectable({ providedIn: 'root' }) export class SearchService { private readonly http = inject(HttpClient); search(query$: Observable<string>): Observable<SearchResult[]> { return query$.pipe( debounceTime(300), distinctUntilChanged(), filter((q) => q.length >= 2), switchMap((q) => this.http.get<SearchResult[]>(`/api/search?q=${q}`).pipe( catchError(() => of([])) // recover from HTTP errors ) ), shareReplay(1) ); } } // Component @Component({ template: ` <input [formControl]="queryControl" /> <ul> <li *ngFor="let result of results$ | async">{{ result.name }}</li> </ul> `, }) export class SearchComponent { private searchService = inject(SearchService); private destroyRef = inject(DestroyRef); queryControl = new FormControl(''); results$ = this.searchService.search(this.queryControl.valueChanges as Observable<string>); }
// takeUntilDestroyed for imperative subscriptions @Component({...}) export class DashboardComponent { private destroyRef = inject(DestroyRef); private statsService = inject(StatsService); stats: Stats | null = null; ngOnInit(): void { this.statsService.getStats().pipe( takeUntilDestroyed(this.destroyRef) ).subscribe(stats => { this.stats = stats; }); } }
Details
Flattening operators compared:
| Operator | Behavior | Use when |
|---|---|---|
| Cancels previous inner observable on new emission | Search, route params, latest-only |
| Queues — waits for previous to complete | Sequential saves, ordered requests |
| All concurrent, results interleaved | Fire-and-forget, parallel independent |
| Ignores new emissions while inner is active | Submit button, login — prevent double-submit |
BehaviorSubject pattern:
@Injectable({ providedIn: 'root' }) export class CartService { private _items = new BehaviorSubject<CartItem[]>([]); readonly items$ = this._items.asObservable(); add(item: CartItem): void { this._items.next([...this._items.value, item]); } }
Error boundary in services: Use
catchError inside the inner observable (inside switchMap) rather than at the top level. This keeps the outer stream alive so subsequent events continue to work after an error:
switchMap((id) => this.http.get(`/api/item/${id}`).pipe( catchError((err) => { this.notificationService.error(err.message); return of(null); }) ) );
pitfall: shareReplay
shareReplay(1) without refCount: true keeps the subscription alive even after all consumers unsubscribe. For HTTP calls this is usually acceptable. For WebSocket or timer streams, use shareReplay({ bufferSize: 1, refCount: true }) to allow cleanup.
Avoiding
pipe duplication: Multiple async
| async pipes on the same observable in a template create multiple subscriptions. Extract into one subscription with *ngIf="results$ | async as results" or use the ng-container pattern.
vs manual teardown: The legacy pattern used a takeUntilDestroyed
Subject destroyed in ngOnDestroy:
private destroy$ = new Subject<void>(); obs$.pipe(takeUntil(this.destroy$)).subscribe(...); ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }
takeUntilDestroyed(this.destroyRef) eliminates the boilerplate and works in services too (not just components).
Source
https://angular.dev/guide/rxjs-best-practices
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.