Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular NgRx - Effect to continue polling a service only called the first time

I have an application where I have just added NgRX where I wish to use effects to switch polling on and off.

Sample outline

I followed this post which seemed like a good approach. I have a simplified example of this here, with the bulk of the code is in app.effects.ts.

Similar to the example, I have the effects startPolling$, stopPolling$ and continuePolling$, except I am using the newer createEffect factory methods.

Also, I have moved the delay(2000)above the takeWhile(), as I found if the service call throws an error, the the catchError(err => of(appActions.getDataFail(err))) would cause the effect to go into an continuous very fast loop without the delay.

The start and stop button dispatches the polling start and stop...

public start() {
    console.log('dispatching start');
    this.store.dispatch(appActions.startPolling());
  }

  public stop() {
    console.log('dispatching stop');
    this.store.dispatch(appActions.stopPolling());
  }

My Problem

I have some console logs so we can see what is going on.

When we click the start button (just the first time), I can see the polling start, and continue as expected. Eg I can see the following over and over...

dispatching start
app effect started polling
app.service.getData
app effect continue polling
app.service.getData
app effect continue polling
app.service.getData
app effect continue polling

Perfect.

And when I hit the stop I see

dispatching stop
app effect stop polling

Also correct.

Now, the problem, is when I try to restart. If I now click the start button again, all I see is the initial start polling effect...

dispatching start
app effect started polling
app.service.getData

and the code in continuePolling$is no longer being called, so I have no polling.

Does anyone have any idea why this effect is not triggered the seconds time? I just cannot work out why this is.

Update 1

I think perhaps my problem is that once isPollingActive is set to false, and takeWhile(() => this.isPollingActive), "stops", the observable is no longer active, ie the continuePolling$ complete, so will never restart?

Assuming this, I tried the following where I have 2 different variables, one to "pause" the polling (eg if I detect the app in an offline mode), and another to cancel (ie when the user would navigate out of the component).

So, my whole effects now becomes...

    @Injectable()
    export class AppEffects {
      private isPollingCancelled: boolean;
      private isPollingPaused: boolean;

      constructor(
        private actions$: Actions,
        private store: Store<AppState>,
        private appDataService: AppDataService
      ) { }

      public startPolling$ = createEffect(() => this.actions$.pipe(
        ofType(appActions.startPolling),
        tap(_ => console.log('app effect started polling')),
        tap(() => {
          this.isPollingCancelled = false;
          this.isPollingPaused = false;
        }),        
          mergeMap(() =>
            this.appDataService.getData()
              .pipe(                        
                switchMap(data => {              
                  return [appActions.getDataSuccess(data)
                  ];
                  }),
                catchError(err => of(appActions.getDataFail(err)))
              ))
        ));

         public pausePolling$ = createEffect(() => this.actions$.pipe(
            ofType(appActions.pausePolling),
            tap(_ => this.isPollingPaused = true),
            tap(_ => console.log('app effect pause polling')),       
         ));
      
      public cancelPolling$ = createEffect(() => this.actions$.pipe(
        ofType(appActions.cancelPolling),
        tap(_ => this.isPollingCancelled = true),
        tap(_ => console.log('app effect cancel polling')),
      ));

        public continuePolling$ = createEffect(() => this.actions$.pipe(
          ofType(appActions.getDataSuccess, appActions.getDataFail),    
          tap(data => console.log('app effect continue polling')),  
          takeWhile(() => !this.isPollingCancelled),    
          delay(3000),  
     
          mergeMap(() =>
            this.appDataService.getData()
              .pipe(   
                delay(3000),  
                tap(data => console.log('app effect continue polling - inner loop')),  
                takeWhile(() => !this.isPollingPaused), // check again incase this has been unset since delay 
                switchMap(data => {              
                  return [appActions.getDataSuccess(data)
                  ];
                  }),
                catchError(err => of(appActions.getDataFail(err)))
              ))
        ));    
    } 

I would not recommend running the above as when I then dispatch a pause polling action, the effect seem to get into an endless loop, and I have to kill the browser via task manager.

I have no ideas why this is happening, but I appear to be further from a solution than before.

Update 2

I noticed I was not returning any actions from the pause and cancel effects.

So I have updated them we follows...

 public pausePolling$ = createEffect(() => this.actions$.pipe(
    ofType(appActions.pausePolling),
    tap(_ => this.isPollingPaused = true),
    tap(_ => console.log('app effect pause polling')),
    map(_ => appActions.pausePollingSuccess())
  ));
  
  public cancelPolling$ = createEffect(() => this.actions$.pipe(
    ofType(appActions.cancelPolling),
    tap(_ => {
      this.isPollingCancelled = true;
      this.isPollingPaused = true;
    }),
    tap(_ => console.log('app effect cancel polling')),
    map(_ => appActions.cancelPollingSuccess())
  ));

Now the pause seems to work OK, but when I dispatch the appActions.cancelPolling, I again see like an infinite loop of app effect cancel polling being logged to the console.

Update 3

I have found why I get the infinite loop and how to stop it. According to the doco here, I can add the dispatch:false...

    public cancelPolling$ = createEffect(() => this.actions$.pipe(
        ofType(appActions.cancelPolling),
        tap(_ => {
          this.isPollingCancelled = true;
          this.isPollingPaused = true;
        }),
        tap(_ => console.log('app effect cancel polling')),
      ), { dispatch: false }); // <------ add this

and this seems to fix my infinite loop.

My only task now is to be able to work out how to be able to start, stop, and restart the polling handling both success calls to appDataService.getData() as well as for exceptions.

I can get it working for one or the other (depending on where I put the delay and takewhile), but not for both

Update 4

I have the latest code here.

Running it as is, I have the getData succeed, and surprisingly, either the pause OR stop action will stop it and allow it to restart.. I am surprised the stop action allows it to restart, as I was assuming the takeWhile(() => !this.isPollingCancelled), would cancel the effect.

Also, if trueis passed to getData this will cause it's observable to error. The polling continues (as wanted, ie still retry even on error), but once we now when we dispatch the pause action, it does NOT stop polling, and it we dispatch the stop, it DOES stop, but then it will not restart. I cannot win.

Update 5

I thought perhaps since the continue polling effect gets cancelled, I could just recreate it each time, as below..

    import { Injectable, OnInit, OnDestroy } from '@angular/core';
    import { createEffect, Actions, ofType } from '@ngrx/effects';
    import { select, Store } from '@ngrx/store';
    import { mergeMap, map, catchError, takeWhile, delay, tap, switchMap } from 'rxjs/operators';
    import { AppState } from './app.state';
    import { Observable, of } from 'rxjs';
    import { AppDataService } from '../app-data.service';
    import * as appActions from './app.actions';

    @Injectable()
    export class AppEffects {
      private isPollingCancelled: boolean;
      private isPollingPaused: boolean;

      constructor(
        private actions$: Actions,
        private store: Store<AppState>,
        private appDataService: AppDataService
      ) { }

      public startPolling$ = createEffect(() => this.actions$.pipe(
        ofType(appActions.startPolling),
        tap(_ => console.log('app effect started polling')),
        tap(() => {
          this.isPollingCancelled = false;
          this.isPollingPaused = false;
          this.createPollingEffect(); // <--- recreate the effect every time
        }),        
          mergeMap(() =>
            this.appDataService.getData()
              .pipe(                        
                switchMap(data => {              
                  return [appActions.getDataSuccess(data)
                  ];
                  }),
                catchError(err => of(appActions.getDataFail(err)))
              ))
        ));

      public pausePolling$ = createEffect(() => this.actions$.pipe(
        ofType(appActions.pausePolling),
        tap(_ => this.isPollingPaused = true),
        tap(_ => console.log('app effect pause polling')),
      ), { dispatch: false });
      
      public cancelPolling$ = createEffect(() => this.actions$.pipe(
        ofType(appActions.cancelPolling),
        tap(_ => {
          this.isPollingCancelled = true;
          this.isPollingPaused = true;
        }),
        tap(_ => console.log('app effect cancel polling')),
      ), { dispatch: false });

      public continuePolling$: any;

      private createPollingEffect(): void {
        console.log('creating continuePolling$');
        this.continuePolling$ = createEffect(() => this.actions$.pipe(
          ofType(appActions.getDataSuccess, appActions.getDataFail),
          tap(data => console.log('app effect continue polling')),
          delay(3000),
          takeWhile(() => !this.isPollingCancelled),
          mergeMap(() =>
            this.appDataService.getData(false)
              .pipe(
                tap(data => console.log('app effect continue polling - inner loop')),

                switchMap(data => {
                  return [appActions.getDataSuccess(data)
                  ];
                }),
                catchError(err => of(appActions.getDataFail(err)))
              ))
        ), { resubscribeOnError: true });
      } 
    }

So, in the startPolling I call this.createPollingEffect() to create the continue polling effect.

However, when I tried this, the polling never starts.

Update 6

I have come up with a solution that seems to work for me.

I have the following

public startPolling$ = createEffect(() => this.actions$.pipe(
        ofType(dataActions.startPollingGetData),
        tap(_ => this.logger.info('effect start polling')),
        tap(() => this.isPollingActive = true),
        switchMap(_ => this.syncData())
      ), { dispatch: false });
      
    public continuePolling$ = createEffect(() => this.actions$.pipe(
        ofType(dataPlannerActions.DataSuccess,
          dataActions.DataFail),
        tap(_ => this.logger.debug('data effect continue polling')),
        tap(_ => this.isInDelay = true),
        delay(8000),
        tap(_ => this.isInDelay = false),
        switchMap(_ => this.syncData())
      ), { dispatch: false });


    public stopPolling$ = createEffect(() => this.actions$.pipe(
        ofType(dataActions.stopPollingData),
        tap(_ => this.isPollingActive = false),
        tap(_ => this.logger.info('data effect stop polling')),
        map(_ => dataActions.stopPollingDataSuccess())
      ), { dispatch: false });


    private syncData(): Observable<Action> {
        const result$: Observable<Action> = Observable.create(async subscriber => {
          try {
            // If polling "switched off", we just need to return anything (not actually used)
            // Id isInDelay, we may be restating while we still have a pending delay.
            // In this case we will exit, and just wait for the delay to restart
            // (otherwise we can end up with more than one call to this)
            if (this.isInDelay || !this.isPollingActive) {
              subscriber.next("");
              return;
            }

I use a couple of "flags" here, I am sure thee would be a more "rxy" way of doing this.

In fact, see this post on how to possibly get rid of the isInDelay (I Just need to get around to putting this into my production code above)

like image 201
peterc Avatar asked Oct 17 '19 06:10

peterc


People also ask

When should you not use NgRx?

When should you not use NgRx? Never use NgRx if your application is a small one with just a couple of domains or if you want to deliver something quickly. It comes with a lot of boilerplate code, so in some scenarios it will make your coding more difficult.

How does NgRx effect work?

Most effects are straightforward: they receive a triggering action, perform a side effect, and return an Observable stream of another action which indicates the result is ready. NgRx effects will then automatically dispatch that action to trigger the reducers and perform a state change.

What are the benefits of NgRx effects?

It provides several advantages by simplifying your application state to plain objects, enforcing unidirectional data flow, and more. The Ngrx/Effects library allows the application to communicate with the outside world by triggering side effects.

What are side effects in NgRx?

A side effect refers simply to the modification of some kind of state - for instance: Changing the value of a variable; Writing some data to disk; Enabling or disabling a button in the User Interface.


1 Answers

Use that instead:

public startPolling$ = createEffect(() => this.actions$.pipe(
  ofType(appActions.startPolling),    
  tap(_ => console.log('app effect started polling')),  
  tap(() => this.isPollingActive = true),        
  switchMap(() =>
    this.appDataSurvice.getData()
      .pipe(                        
        exhaustMap(data => {              
          return [appActions.getDataSuccess(data)];
        }),
        catchError(err => of(appActions.getDataFail(err)))
      ))
));
like image 133
MoxxiManagarm Avatar answered Sep 26 '22 13:09

MoxxiManagarm