1

I have two side effects. One of them presents a global loading spinner and the other one dismisses the spinner again. For presenting and dismissing the loader I have created a LoadingService. Note that the presentLoader() and dismissLoader() methods are async. Reason is that the service encapsulates the LoadingController of the Ionic framework.

import { Injectable } from '@angular/core';
import { LoadingController } from '@ionic/angular';

@Injectable({
  providedIn: 'root'
})
export class LoadingService {

  private loader?: HTMLIonLoadingElement;

  constructor(private loadingController: LoadingController) {
  }

  public async presentLoader(translationKey: string): Promise<void> {
    if (this.loader) {
      this.loader.message = translationKey;
      return;
    }
    this.loader = await this.loadingController.create({
      message: translationKey
    });
    await this.loader.present();
  }

  public async dismissLoader(): Promise<void> {
    await this.loader?.dismiss();
    this.loader = undefined;
  }
}

In my Application i work with the RequestStarted, RequestSucceeded and RequestFailed Action naming pattern.

public updateMyThingsItem$ = createEffect(() => {
  return this.actions$.pipe(
    ofType(
      MyThingsItemActions.updateMyThingsItemNameRequested,
      MyThingsItemActions.updateMyThingsItemPurchasePriceRequested,
      MyThingsItemActions.updateMyThingsItemPurchaseDateRequested
    ),
    exhaustMap((action) =>
      this.myThingsIntegrationService.patchMyThingsItem(action.myThingsItem.id, action.myThingsItem.changes).pipe(
        map((myThingsItem) => MyThingsItemActions.updateMyThingsItemSucceeded({ myThingsItem })),
        catchError((error: string) => of(MyThingsItemActions.updateMyThingsItemFailed({ error })))
      )
    )
  );
});

Now I created a side effect to display a loader for a couple of RequestStarted actions and a side effect that hides the loader again for the related Succeeded and Failed actions. This looks something like this:

public presentLoadingSpinner$ = createEffect(() => {
  return this.actions$.pipe(
    ofType(
      MyThingsItemActions.updateMyThingsItemNameRequested,
      MyThingsItemActions.updateMyThingsItemPurchasePriceRequested,
      MyThingsItemActions.updateMyThingsItemPurchaseDateRequested
    ),
    exhaustMap(() => this.loadingService.presentLoader('loading...'))
  );
}, {
  dispatch: false
});

public hideLoadingSpinner$ = createEffect(() => {
  return this.actions$.pipe(
    ofType(
      MyThingsItemActions.updateMyThingsItemSucceeded,
      MyThingsItemActions.updateMyThingsItemFailed
    ),
    exhaustMap(() => this.loadingService.dismissLoader())
  );
}, {
  dispatch: false
});

Now for my problem: Because the methods in my loadingService are async there is the possibility that the side effect to dismiss the loader is triggered before the presentLoader() method is completed and the loader does not yet exist in the DOM. This happens when the API is really quick or a client error (no network) happens and the Failure Action is dispatched before the loader is created.

I therefore realise that I now have depending effects. But how else could I solve the problem with a side effect that triggers async code?

I know that I could create a loading state with a isLoading flag. But I would have the same problem when creating the effect from a selector.

An other possibility would be to create an overlaying Loader Component in the AppComponent that is rendered on the isLoading flag. But then I would have to give up the LoadingController of the Ionic framework which I do not really like.

I would like to know how to solve these problems in general when calling a side effect method that is async.

Pascal
  • 75
  • 8
  • Is that something that maybe my `LoadingService` should solve instead? Meaning that the `LoadingService` is stateful and I should wait for the existence of a LoadingElement before dismissing? – Pascal Jan 27 '22 at 14:31

0 Answers0