Search code examples
rxjsngrxrxjs6ngrx-effectsrxjs-pipeable-operators

NgRx effect call API and dispatch action in a loop


Hellow, I have below Json structure, which is provided as a payload in the UpdateOrders action. In the effect, I would like to iterate over the reservations and orders, call the this.orderApiService.updateOrder service and dispatch a UpdateOrderProgress action. In the UpdateOrderProgress action I would like to provide the numberOfReservationsUpdated and the totalReservations

const reservationOrders = [
    {
        reservationNumber: '22763883',
        orders: [
            {
                orderId: 'O12341',
                amount: 25
            },
            {
                orderId: 'O45321',
                amount: 50
            }
        ]
    },
    {
        reservationNumber: '42345719',
        orders: [
            {
                orderId: 'O12343',
                amount: 75
            }
        ]
    }
];

I have the following effect to achieve this, but unfortunately, this effect does not work and throws an exception.

@Effect()
updateOrders$ = this.actions$.pipe(
    ofType<UpdateOrders>(UpdateOrdersActionType.UPDATE_ORDERS),
    filter((action) => !!action.reservationOrders),
    exhaustMap((action) => {
        return combineLatest(action.reservationOrders.map((x, index) => {
            const totalReservations = action.reservationOrders.length;
            const numberOfReservationsUpdated = index + 1;
            return combineLatest(x.orders.map((order) => {
                const orderUpdateRequest: OrderUpdateRequest = {
                    orderId: order.orderId,
                    amount: order.amount
                };
                return this.orderApiService.updateOrder(orderUpdateRequest).pipe(
                    switchMap(() => [new UpdateOrderProgress(numberOfReservationsUpdated, totalReservations)]),
                    catchError((message: string) => of(console.info(message))),
                );
            }))
        }))
    })
);

How can I achieve this? Which RxJs operators am I missing?


Solution

  • Instead of using combineLatest, you may switch to using a combination of merge and mergeMap to acheive the effect you're looking for.

    Below is a representation of your problem statement -

    1. An action triggers an observable
    2. This needs to trigger multiple observables
    3. Each of those observables need to then trigger some action (UPDATE_ACTION)

    One way to achieve this is as follows -

    const subj = new Subject<number[]>();
    
    const getData$ = (index) => {
      return of({
        index,
        value: 'Some value for ' + index,
      }).pipe(delay(index*1000));
    };
    
    const source = subj.pipe(
      filter((x) => !!x),
      exhaustMap((records: number[]) => {
        const dataRequests = records.map((r) => getData$(r));
        return merge(dataRequests);
      }),
      mergeMap((obs) => obs)
    );
    
    source.subscribe(console.log);
    
    subj.next([3,1,1,4]); // Each of the value in array simulates a call to an endpoint that'll take i*1000 ms to complete
    
    // OUTPUT - 
    // {index: 1, value: "Some value for 1"}
    // {index: 1, value: "Some value for 1"}
    // {index: 3, value: "Some value for 3"}
    // {index: 4, value: "Some value for 4"}
    

    Given the above explaination, your code needs to be changed to something like -

    const getOrderRequest$ = (order: OrderUpdateRequest, numberOfReservationsUpdated, totalReservations) => {
        const orderUpdateRequest: OrderUpdateRequest = {
            orderId: order.orderId,
            amount: order.amount
        };
        return this.orderApiService.updateOrder(orderUpdateRequest).pipe(
            switchMap(() => new UpdateOrderProgress(numberOfReservationsUpdated, totalReservations)),
            catchError((message: string) => of(console.info(message))),
        );
    }
    
    updateOrders$ = this.actions$.pipe(
        ofType<UpdateOrders>(UpdateOrdersActionType.UPDATE_ORDERS),
        filter((action) => !!action.reservationOrders),
        exhaustMap((action) => {
    
            const reservationOrders = action.reservationOrders;
            const totalLen = reservationOrders.length
            const allRequests = []
            reservationOrders.forEach((r, index) => {
                r.orders.forEach(order => {
                    const req = getOrderRequest$(order, index + 1, totalLen);
                    allRequests.push(req);
                });
            });
    
            return merge(allRequests)
        }), 
        mergeMap(obs=> obs)
    );
    

    Side Note - While the nested observables in your example may work, there are chances that you'll be seeing wrong results due to inherent nature of http calls taking unknown amount of time to complete. Meaning, the way you've written it, there are chances that you can see in some cases that numberOfReservationsUpdated as not an exact indicative of actual number of reservations updated.

    A better approach would be to handle the state information in your reducer. Basically, pass the reservationNumber in the UPDATE action payload and let the reducer decide how many requests are pending completion. This will be an accurate representation of the state of the system. Also, it will simplify your logic in @effect to a single nested observable rather than multiple nesting.