I'm working on an Angular application where I fetch data from an API and display it using my custom state pipe and async pipe. There is a button to refresh the API call. However, if an error occurs, the observable stops emitting values and I can't refresh it anymore.
state.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
import { Observable, catchError, map, of, startWith } from 'rxjs';
type ObsState<T> = Observable<ObsStateAsync<T>>;
export type ObsStateAsync<T> = { loading: boolean; result?: T; error?: any };
@Pipe({
name: 'state',
})
export class StatePipe implements PipeTransform {
transform<T>(val: Observable<T>): ObsState<T> {
return val.pipe(
map((value: any) => ({ loading: false, result: value })),
startWith({ loading: true }),
catchError((error) => of({ loading: false, error }))
);
}
}
@Component({
selector: 'app-root',
template: `
<button (click)="onRefresh()">Refresh</button>
@let breedState = breeds$ | state | async;
@if(breedState?.loading){
Fetching ...
}
@if(breedState?.error){
Error
}
@if(breedState?.result){
@for(breed of breedState?.result;track $index){
<div>
{{ breed.attributes.name }}
</div>
}
}
`,
imports: [StatePipe, AsyncPipe],
})
export class App {
private readonly httpClient = inject(HttpClient);
refresh = new BehaviorSubject<void>(undefined);
getBreed$ = this.httpClient
.get('https://dogapi.dog/api/v2/breeds') // Modify API endpoint to create an error
.pipe(map((res: any) => res.data));
breeds$ = this.refresh.asObservable().pipe(switchMap(() => this.getBreed$));
onRefresh(): void {
this.refresh.next();
}
}
And I don't wanna do the catchError(()=>of([]))
thing because the error needs to be displayed on the UI.
You have to place the catchError
inside the pipe of the http observable, so I suggest a rxjs
reusable pipe operator stateTransform
which basically does the same thing, but is inside the pipe of the http.get
.
type ObsState<T> = Observable<ObsStateAsync<T>>;
export type ObsStateAsync<T> = { loading: boolean; result?: T; error?: any };
function stateTransform<T>(source$: Observable<T>): ObsState<T> {
return source$.pipe(
startWith({ loading: true }),
map((value: any) => ({ loading: false, result: value })),
catchError((error) => of({ loading: false, error }))
);
}
You just need to add this reusable operator below the map
transform.
getBreed$ = this.httpClient
.get<BreedResult>('https://dogapi.dog/api/v2/breedss') //modify api endpoint to create error
.pipe(
map((res: BreedResult) => res.data),
stateTransform
);
import { Component, inject } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { HttpClient, provideHttpClient } from '@angular/common/http';
import { StatePipe } from './state.pipe';
import { AsyncPipe } from '@angular/common';
import {
BehaviorSubject,
map,
switchMap,
Observable,
startWith,
catchError,
of,
} from 'rxjs';
export interface BreedResult {
data: Array<any>;
}
@Component({
selector: 'app-root',
template: `
<button (click)="onRefresh()">Refresh</button>
@let breedState = breeds$ | async;
@if(breedState?.loading){
Fetching ...
} @else if(breedState?.error){
Error
} @else if(breedState?.result){
@for(breed of breedState?.result;track $index){
<div>
{{breed.attributes.name}}
</div>
}
}
`,
imports: [StatePipe, AsyncPipe],
})
export class App {
private readonly httpClient = inject(HttpClient);
refresh = new BehaviorSubject<void>(undefined);
getBreed$ = this.httpClient
.get<BreedResult>('https://dogapi.dog/api/v2/breedss') //modify api endpoint to create error
.pipe(
map((res: BreedResult) => res.data),
stateTransform
);
breeds$ = this.refresh.asObservable().pipe(switchMap(() => this.getBreed$));
onRefresh(): void {
this.refresh.next();
}
}
bootstrapApplication(App, { providers: [provideHttpClient()] });
type ObsState<T> = Observable<ObsStateAsync<T>>;
export type ObsStateAsync<T> = { loading: boolean; result?: T; error?: any };
function stateTransform<T>(source$: Observable<T>): ObsState<T> {
return source$.pipe(
startWith({ loading: true }),
map((value: any) => ({ loading: false, result: value })),
catchError((error) => of({ loading: false, error }))
);
}