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.
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.
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.
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
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 true
is 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.
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.
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)
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.
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.
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.
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.
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)))
))
));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With