Search code examples
angularasynchronousrxjsngrxrxjs-observables

NGRX effects, mapping and chained API calls


I'm having trouble figuring out the following;

My checkout process consists of a few steps when picking an alternative shipping address. We're using CommerceTools, CommerceTools works with "actions" to change the cart data.

1st action: Reset the shipping method, set the new address on the cart. 2nd action: Get the matching shipping methods. 3rd action: Get the default shipping method id. 4th action: Set the default shipping method by ID.

All of these actions require an API call to CommerceTools.

To handle these requests and to manage the state, I'm using NGRX. And the biggest problem I'm facing right now is that chained effects say that there is a new value being emitted, when the HTTP request for the API call isn't showing up in my network tab. Therefor returning "old" data.

Example: After performing the 1st action, the 2nd action is fired. It says that "getShippingMethodsSuccess" is triggerd (which triggers on an http get observable). Except, this HTTP call never shows up in my network tab and the old shipping method is getting picked, resulting in an error on CommerceTools' end.

These are my effects:

  runSetShippingAddress$ = createEffect(() =>
    this.actions$.pipe(
      ofType(fromActions.setShippingAddress),
      switchMap((payload: any) =>
        this._orderService.updateOrder(payload.cart, payload.actions, payload.cart.version).pipe(
          switchMap((cart: Cart) => [
              fromActions.setShippingAddressSuccess({cart}),
              fromActions.getShippingMethods({cart})
            ]
          ),
          catchError((err) => of(fromActions.setShippingAddressFailed({error: err})))
        )
      )
    )
  );

  runGetShippingMethods$ = createEffect(() =>
    this.actions$.pipe(
      ofType(fromActions.getShippingMethods),
      mergeMap((payload: any) => this._addressService.getShippingMethods(payload.cart)
        .pipe(
          switchMap((shippingMethods: ShippingMethod[]) => [
            fromActions.getShippingMethodsSuccess({shippingMethods: shippingMethods}),
          ]),
          catchError((err) => of(fromActions.getShippingMethodsFailed({error: err})))
        )
      )
    )
  );

  setDefaultShippingMethod$ = createEffect(() =>
    this.actions$.pipe(
      ofType(fromActions.getShippingMethodsSuccess),
      withLatestFrom(this._store.select('Cart')),
      mergeMap(([action, state]) =>
        from(this._addressService.getDefaultShippingMethod(action.shippingMethods)).pipe(
          tap((defaultShippingMethod) => {
            console.log(defaultShippingMethod);
          }),
          mergeMap((shippingMethod: ShippingMethod) => [
            fromActions.updateOrderData({
              cart: state.cart,
              actions: [
                {
                  action: 'setShippingMethod',
                  data: {
                    key: 'shippingMethod',
                    value: {id: shippingMethod.id, typeId: 'shipping-method'},
                  },
                },
              ]
            }),
          ]),
          catchError((err) => {
            console.log(err); // Log the error here
            return of(fromActions.updateOrderDataFailed({error: err}));
          })
        )
      )
    )
  );

updateOrderData$ = createEffect((): any =>
    this.actions$.pipe(
      ofType(fromActions.updateOrderData),
      mergeMap((payload: any) => this._orderService.updateOrder(payload.cart, payload.actions)
        .pipe(
          switchMap((data: Cart) => [
            fromActions.updateOrderDataSuccess({updatedCart: data})
          ]),
          catchError((err) => {
            // If the cart has a wrong version, CommerceTools throws a 409. Get version from the error message, and run again!
            if (err.status === 409) {
              console.log('%c---version mismatch retry update order---', 'background: red; color: white; padding: 3px; border-radius: 2px;');
              const newVersionNumber: number = err.error.errors[0].currentVersion;
              return of(fromActions.updateOrderData({
                cart: payload.cart,
                actions: payload.actions,
                version: newVersionNumber
              }));
            } else {
              return of(fromActions.updateOrderDataFailed({error: err}));
            }
          })
        )
      )
    )
  )

This is my getShippinMethods API call:

  getShippingMethods(cart: Cart ) {
    const url = environment.commercetoolsURL + '/' +
      environment.commercetoolsProjectKey + '/shipping-methods/matching-cart?cartId=' + cart.id;
    return this.http.get(url).pipe(map((data: any) => data.results));

updateOrder looks like this:

  updateOrder(cart: Cart, actionData: [Action], version?: number) {
    let actionsArray: any[] = [];
    if (actionData) {
      actionsArray = actionData.map((x) => {
        const obj = x.data ? {
          action: x.action,
          [x.data.key]: x.data.value
        } : {
          action: x.action
        };

        if (x.data && x.data.name) {
          obj.name = x.data.name;
        }

        return obj;
      });
    }

    const createRequest = {
      uri: '/' + environment.commercetoolsProjectKey + '/me/carts/' + cart.id,
      method: 'POST',
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json'
      },
      body: {
        version: version ? version : cart.version,
        actions: actionsArray
      }
    };

    const url = environment.commercetoolsURL + createRequest.uri;

    return this.http.post(url, createRequest.body);
  }

I'm almost certain it has something to do with how I'm handling async value emissions with my pipe functionalities. But I can't put my finger on it.

There are also instances where it does work! For example when trying a few times switching addresses. OR when switching to an address which gives the error, wait a few seconds and then switch addresses again. After that, quickly switching addresses is no problem anymore.

Any help is greatly appreciated.

Good to know: Using Angular v17, rxjs 7.8.1 (compat ^6.6.7), ngrx 16.3.0

I've tried:

  • Delays between actions
  • Subscribe to the HTTP call first, then subscribe to the result and return as promise
  • Creating extra effects to trigger on "success" effects
  • Subscribe to the success action within my page and trigger the getShippingMethods() that way
  • Switchmap/mergemap/concatmap/concat

All in an effort to get results right away and get the http call to trigger.


Solution

  • This answer may be disappointing but it was a cache problem. Only thing is, caching is not enabled in my request header in any shape or form. Nor is my HTTP interceptor caching data for me.

    The way I solved this issue is to add a timestamp as param to my request: this.http.get(url + '&timestamp=' + new Date().getTime()).

    If anyone can elaborate on that, please feel free. But for now my problem is solved.