From harness-claude
Applies RxJS patterns in Angular: switchMap for HTTP requests, takeUntilDestroyed for cleanup, async pipe for templates, catchError for errors. For reactive data fetching, state management, and subscription handling.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Apply RxJS patterns correctly in Angular — switchMap for HTTP, takeUntilDestroyed for cleanup, async pipe for templates, and catchError for resilience
Provides RxJS code patterns for Angular apps: observable creation, HttpClient integration, transformation operators like map/switchMap, and filtering like take/filter for async operations.
Provides expert Angular/TypeScript patterns for standalone components, signals, RxJS, NgRx state management, smart/dumb components, and performance.
Manages reactive state in Angular 17+ using signal(), computed(), effect(), and toSignal() for fine-grained, zone-free reactivity without manual subscriptions.
Share bugs, ideas, or general feedback.
Apply RxJS patterns correctly in Angular — switchMap for HTTP, takeUntilDestroyed for cleanup, async pipe for templates, and catchError for resilience
takeUntilDestroyed(this.destroyRef) (Angular 16+) instead of ngOnDestroy + Subject teardown patterns.switchMap when a new event should cancel the previous in-flight request (e.g., search typeahead). Use concatMap when order matters and requests must not overlap. Use mergeMap when all concurrent requests are independent.async pipe in templates instead of manual subscriptions in the component class. It handles subscribe, unsubscribe, and change detection automatically.shareReplay(1) to prevent duplicate requests when multiple consumers subscribe.catchError inside a pipe() chain. Return of(fallbackValue) to recover, or throwError(() => err) to propagate. Never swallow errors silently.BehaviorSubject for state that needs an initial value and synchronous read (.value). Expose only the observable side via asObservable() — keep .next() private to the service.subscribe() inside subscribe()). Flatten with switchMap, mergeMap, or combineLatest.debounceTime(300) before triggering HTTP requests. Pair with distinctUntilChanged() to skip identical values.@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;
});
}
}
Flattening operators compared:
| Operator | Behavior | Use when |
|---|---|---|
switchMap | Cancels previous inner observable on new emission | Search, route params, latest-only |
concatMap | Queues — waits for previous to complete | Sequential saves, ordered requests |
mergeMap | All concurrent, results interleaved | Fire-and-forget, parallel independent |
exhaustMap | 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);
})
)
);
shareReplay pitfall: 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 async pipe duplication: Multiple | 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.
takeUntilDestroyed vs manual teardown: The legacy pattern used a 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).
https://angular.dev/guide/rxjs-best-practices