angulartypescriptrxjsobservable

How to Handle HTTP Calls with Derived Data Using Observables in Angular


In an Angular project, I am trying to retrieve data via an HTTP call. Simultaneously, I need to have an observable whose data is derived from the data obtained through the HTTP call. This way, when the first set of data is updated, the second set will be automatically updated as well. In the example, I have created Item as an object retrieved (a type consisting of id and name), while the derived data is the list of names. Moreover, in this context, I wouldn't even know how to handle the forceUpdate method.

this is what I tried. I am not sure if it is solid code or if it avoids memory leaks. I am also not sure if it is readable enough.

itemList$: BehaviorSubject<Item[]> = new BehaviorSubject<Item[]>([]);
  http = inject(HttpClient);

  constructor() {}

  fetchList$(filter: string): Observable<Item[]> {
    return this.http.get<Item[]>('/api/list');
  }

  getItemList$(filter: string): Observable<Item[]> {
    if (this.itemList$.getValue().length === 0) {
      this.fetchList$(filter)
        .pipe(
          shareReplay(1),
          take(1),
          tap((res) => this.itemList$.next(res))
        )
        .subscribe();
    }
    return this.itemList$.asObservable();
  }

  getItemNames$(): Observable<string[]> {
    return this.itemList$.pipe(
      map((items) => items.map((item) => item.name)),
      map((names) => Array.from(new Set(names)))
    );
  }

  forceUpdate() {
    //how to hanlde this one?
  }

this is the component

 budgetService = inject(BudgetService);

  itemList$ = this.budgetService.getItemList$('test');
  nameList$ = this.budgetService.getItemNames$();

  update() {
    this.budgetService.forceUpdate();
  }

and this the html with the async pipe

<div class="content">
      <div>
        @for (item of itemList$ | async; track $index) {
        {{ item.id }}
        {{ item.name }}
        }
      </div>
      <div>
        @for (name of nameList$ | async; track $index) {
        {{ name }}
        }
      </div>
      <button (click)="update()">Update</button>
    </div>

Solution

  • You could simplify by declaring your filter as a behavior subject and expressing your itemList$ as a derived observable. Your itemNames$ could also be a derived observable, there's no need to wrap in a function that returns observable. Something like this:

    private filter$ = new BehaviorSubject<string>('');
    
    itemList$ = this.filter$.pipe(
      switchMap(filter => this.http.get<Item[]>('api/list')),
      shareReplay(1)
    );
    
    itemNames$ = this.list$.pipe(
      map(items => items.map((item) => item.name)),
      map(names => Array.from(new Set(names)))
    );
    
    setFilter(value: string) {
      this.filter$.next(value);
    }
    

    Here we expose a public method for consumers to set the filter value. We derive the itemList$ from the filter$. So each time filter$ emits, the http.get() call is made with the new filter value (your example code wasn't actually using the filter value, so be sure to add that) and the new list is emitted.

    The itemNames$ observable is derived from the itemList$ as you were already doing.

    Notice there's no explicit subscription. Notice wrapping observables with methods like "get" and "fetch" is not necessary. The "get" is sort of implied when you subscribe to an observable.

    Now, for your forceUpdate() functionality. There are a few ways to go about it. Once simple way would be to have the consumer just call setFilter() which would force an update.

    You could also introduce another BehaviorSubject to act as a refetch trigger:

    private filter$ = new BehaviorSubject<string>('');
    private update$ = new BehaviorSubject<void>(undefined);
    
    itemList$ = combineLatest([this.filter$, this.update$]).pipe(
      switchMap(([filter]) => this.http.get<Item[]>('api/list')),
      shareReplay(1)
    );
    
    private forceUpdate() {
      this.update$.next();
    }
    

    Here we changed the source of itemList$ to actually use two source observables. Whenever either of them emit, a new http call will be made.