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.