Home > Back-end >  Angular and RxJS - merge two HTTP requests, but emit the outer one instantly
Angular and RxJS - merge two HTTP requests, but emit the outer one instantly

Time:10-14

I am fairly new to Angular and RxJS, and I have a hard time wrapping my head around some aspects of RxJS operators. I have some code here (in a component):

this.salesService.getOrders().subscribe(orders => {
  this.orders = orders;
  this.orders.forEach((order, index) => {
    this.salesService.getOrderCustomers(order.id).subscribe(customers => {
      this.orders[index]['customerName'] = customers.map(c => {return c.name}).join(", ");
    })
  });
});

The nice thing about this is that when this.orders is used in a table in my template, it will be available immediately, and then the column containing the customer name will populate as each call to the getOrderCustomers service method completes.

My question is if the same can be expressed with pure RxJS, since the above does not seem to be the "RxJS way", and I would like to learn. Also, in order to isolate the fetching of customers in an effect in the NgRx ComponentStore, my understanding is that the above will not work because the effect method must return an observable.

I have attempted something along these lines:

this.salesService.getOrders().pipe(
    mergeMap(response =>
      from(response.orders).pipe(
        concatMap((oneOrder: object) => {
          return this.salesService.getOrderCustomers(oneOrder['id'])
        }),
        reduce((customers, customer) => [...customers, customer], []),
        map(customerList => {
             /*  ?? Here I would return 'response' with the customer name added into each order, 
                 but I have no indexing of the orders to work with */
        })
      )
    ),

I have two problems - one, I do not know how to add the retrieved customer into the right place in the response.orders variable, and secondly, the outer observable does not seem to emit until all the inner requests are done. Is there any RxJS way to progressively change the outer observable from the inner observable while initially emitting the outer observable in its original state?

I have a feeling that I am approaching this in a wrong, un-Angular and un-RxJS way. Any input would be appreciated. Thank you in advance.

CodePudding user response:

Common problem, for extending array values with another value from a 2nd request.

extendedOrders$ = this.salesService.getOrders().pipe(
  switchMap(orders => {
    return forkJoin(orders.map(order => {
      return this.salesService.getOrderCustomers(order.id).pipe(
        map(customer => ({ ...order, customerName: customer.name })),
      );
    }));
  }),
);

CodePudding user response:

You have observable of orders orders$ = this.salesService.getOrders(). Show them in table with *ngFor="let order of orders$ | async"

Then you have function to fetch some additional order data

getCustomers(order: Order): Observable<Customer> {
  return /* data from service */
}

Then in component template subscribe to customer data for each order and render them {{ getCustomers(order) | async }}

Main benefit is that order data can be rendered already, while additional data is fetched. Of course this can be optimized, customer data can be cached and so on.

And set changeDetection to onPush for better performance.

CodePudding user response:

Here's a solution that doesn't change that much about what you've already done (so it might be easier to understand):

this.salesService.getOrders().pipe(
  mergeMap(orders => 
    merge(
      orders.map((order, index) => this.salesService.getOrderCustomers(order.id).pipe(
        map(customers => customers.map(c => c.name).join(", ")),
        map(customers => ({customers, index}))
      ))
    ).pipe(
      scan((acc, {customers, index}) => {
        const ordersSoFar = [...acc];
        ordersSoFar[index]['customerName'] = customers;
        return ordersSoFar;
      }, orders)
    )
  )
).subscribe(orders => {
  this.orders = orders;
});

The big difference here is that rather then updating orders in the background and letting Angular change detection figure out that orders has changed, this emits the updated orders every time there is an update.

Should work the same to start with, but lets you use the observable directly in your template (without subscribing yourself) if you'd like.

  • Related