Search code examples
angularrxjs

How to use Angular async pipe to display errors and loading statuses on route param change?


In an Angular 18 app using the new template control-flow syntax, how do I use an async pipe to display a loading status when new data it fetched, and how do I make it display errors?

Currently I have a component that watches for changes to route parameters and fetches data based on that, which works well.

@Component({
  selector: 'app-page-demo',
  standalone: true,
  imports: [AsyncPipe, JsonPipe],
  templateUrl: './page-demo.component.html',
})
export class PageDemoComponent implements OnChanges {
  private readonly dataService = inject(DataService);
  private readonly activatedRoute = inject(ActivatedRoute);

  errorMessage = '';

  readonly timesheet$ = this.activatedRoute.paramMap.pipe(
    switchMap((paramMap) => {
      const date = paramMap.get('date');
      return this.dataServicve.getData$(date);
    }),
    catchError((err) => {
      this.errorMessage = err instanceof Error ? err.message : 'Unable to fetch data!';
      return of(undefined);
    }),
    shareReplay({ refCount: true, bufferSize: 1 }),
  );
}
@if (errorMessage === '') {
  <a routerLink="page/2024-09-21">last week</a> | <a routerLink="page/2024-10-05">next week</a>
}

@if (data$ | async; as data) {
  @if (errorMessage === '') {
    <h1>Data loaded!</h1>
    <pre>{{ data | json }}</pre>
  } @else {
    There was an error: {{ errorMessage }}    
  }
} @else {
  Loading...
}

The problem I am having is that when I click on one of the links to navigate to the same route with with a different parameter, the "Loading..." is not displayed while the data is loading as I would expect it to be. It only displays on the initial data load.

Also, I feel like I am not handling errors properly here. This mostly works, but it feels kind of wrong to me and I don't know a better way to do this.


Solution

  • I think you need to add a seperate obs which tracks loading:

    errorMessage = '';
    
    data$ = this.activatedRoute.paramMap.pipe(
        switchMap((paramMap) => {
            const date = paramMap.get('date');
            return this.dataServicve.getData$(date);
        }),
        catchError(err => {
            this.errorMessage = err instanceof Error ? err.message : 'Unable to fetch data!';
            
            // clear the result of previous getData stored by shareReplay
            // also serve the purpose of informing that getData has completed, albeit with error
            return of(null);
        }),
        shareReplay({ refCount: true, bufferSize: 1 }),
    );
    
    // starts loading on param change, stop loading when data$ emits
    loading$ = merge(
        this.activatedRoute.paramMap.pipe(map(() => true),),
        this.data$.pipe(map(() => false)),
    );
    

    then refactor ur html like so.
    eventho the data$ was getting subscribed late, it should still store correct value due to shareReplay

    @if(loading$|async){
        Loading...
    } @else {
        @if (data$ | async; as data) {
            <h1>Data loaded!</h1>
            <pre>{{ data | json }}</pre>
        } @else {
            There was an error: {{ errorMessage }}
        }
    }
    

    I did not test the code above, so improvise if there's mistake on my part