Angular 7 with NGRX - subscription to a store property through selector getting called multiple times

1,201 views
Skip to first unread message

Sagun Pandey

unread,
May 12, 2019, 1:03:59 AM5/12/19
to Angular and AngularJS discussion

I am experiencing strange behavior with my app. I have subscribed to a state property using the selector and what I am seeing is, my subscription is getting called no matter what property in the state changes.

Below is a cleaned-up version of my code. My state has all kinds of properties, some objects, and some flat properties. Selectors for all properties work as expected except `getImportStatus` and `getImportProgress` selectors. **Subscription to these selectors is triggered no matter what property in the store changes.** I am just about to lose my mind. Can anyone suggest what I am doing wrong? Has anyone faced such issue? I know people run into similar issues when they do not unsubscribe the subscriptions. But, in my case, as you can see I am unsubscribing and the event is being triggered for any property change which has got me puzzled.

Here's my reducer:

    import {ImportConfigActions, ImportConfigActionTypes} from '../actions';
    import * as _ from 'lodash';
    import {ImportProgress} from '../../models/import-progress';
    import {ImportStatus} from '../../models/import-status';
    import {ActionReducerMap, createFeatureSelector} from '@ngrx/store';

    export interface ImportState {
      importConfig: fromImportConfig.ImportConfigState;
    }
    export const reducers: ActionReducerMap<ImportState> = {
      importConfig: fromImportConfig.reducer,
    };

    export const getImportState = createFeatureSelector<ImportState>('import');
    
    export interface ImportConfigState {
      spinner: boolean;
      importStatus: ImportStatus; // This is my custom model
      importProgress: ImportProgress; // This is my custom model
    }
    
    export const initialState: ImportConfigState = {
      spinner: false,
      importStatus: null,
      importProgress: null
    };
    
    export function reducer(state = initialState, action: ImportConfigActions): ImportConfigState {
      let newState;
    
      switch (action.type) {
        case ImportConfigActionTypes.ShowImportSpinner:
          newState = _.cloneDeep(state);
          newState.spinner = false;
          return newState;
      
    case ImportConfigActionTypes.HideImportSpinner:
          newState = _.cloneDeep(state);
          newState.spinner = false;
          return newState;
    
        case ImportConfigActionTypes.FetchImportStatusSuccess:
          newState = _.cloneDeep(state);
          newState.importStatus = action.importStatus;
          return newState;
      
        case ImportConfigActionTypes.FetchImportProgressSuccess:
          newState = _.cloneDeep(state);
          newState.importProgress = action.importProgress;
          return newState;
    
        default:
          return state;
      }
    }

Here're my actions:

    import {Action} from '@ngrx/store';
    import {ImportStatus} from '../../models/import-status';
    import {ImportProgress} from '../../models/import-progress';
    
    export enum ImportConfigActionTypes {
      ShowImportSpinner = '[Import Config] Show Import Spinner',
      HideImportSpinner = '[Import Config] Hide Import Spinner',
    
      FetchImportStatus = '[Import Config] Fetch Import Status',
      FetchImportStatusSuccess = '[ImportConfig] Fetch Import Status Success',
      FetchImportStatusFailure = '[Import Config] Fetch Import Status Failure',
      FetchImportProgress = '[Import Config] Fetch Import Progress',
      FetchImportProgressSuccess = '[ImportConfig] Fetch Import Progress Success',
      FetchImportProgressFailure = '[Import Config] Fetch Import Progress Failure'
    }
    
    export class ShowImportSpinner implements Action {
      readonly type = ImportConfigActionTypes.ShowImportSpinner;
    }
    export class HideImportSpinner implements Action {
      readonly type = ImportConfigActionTypes.HideImportSpinner;
    }
    
    export class FetchImportStatus implements Action {
      readonly type = ImportConfigActionTypes.FetchImportStatus;
      constructor(readonly projectId: number, readonly importId: number) {}
    }
    export class FetchImportStatusSuccess implements Action {
      readonly type = ImportConfigActionTypes.FetchImportStatusSuccess;
      constructor(readonly importStatus: ImportStatus) {}
    }
    export class FetchImportStatusFailure implements Action {
      readonly type = ImportConfigActionTypes.FetchImportStatusFailure;
    }
    export class FetchImportProgress implements Action {
      readonly type = ImportConfigActionTypes.FetchImportProgress;
      constructor(readonly projectId: number, readonly importId: number) {}
    }
    export class FetchImportProgressSuccess implements Action {
      readonly type = ImportConfigActionTypes.FetchImportProgressSuccess;
      constructor(readonly importProgress: ImportProgress) {}
    }
    export class FetchImportProgressFailure implements Action {
      readonly type = ImportConfigActionTypes.FetchImportProgressFailure;
    }
    
    
    export type ImportConfigActions =
      ShowImportSpinner | HideImportSpinner |
      FetchImportStatus | FetchImportStatusSuccess | FetchImportStatusFailure |
      FetchImportProgress | FetchImportProgressSuccess | FetchImportProgressFailure;

Here're my effects:

    import {Injectable} from '@angular/core';
    import {Actions, Effect, ofType} from '@ngrx/effects';
    import {ImportConfigService} from '../../services';
    import {from, Observable} from 'rxjs';
    import {Action} from '@ngrx/store';
    import {
      FetchImportProgress, FetchImportProgressFailure, FetchImportProgressSuccess,
      FetchImportStatus, FetchImportStatusFailure, FetchImportStatusSuccess,
      HideImportSpinner,
      ImportConfigActionTypes,
      StartImport
    } from '../actions';
    import {catchError, map, mergeMap, switchMap} from 'rxjs/operators';
    
    @Injectable()
    export class ImportConfigEffects {
    
      constructor(private actions$: Actions, private service: ImportConfigService, private errorService: ErrorService) {}
    
      @Effect()
      startImport: Observable<Action> = this.actions$.pipe(
        ofType<StartImport>(ImportConfigActionTypes.StartImport),
        switchMap((action) => {
          return this.service.startImport(action.payload.projectId, action.payload.importId, action.payload.importConfig)
            .pipe(
              mergeMap((res: any) => {
                if (res.status === 'Success') {
                  return [
                    new HideImportSpinner()
                  ];
                }
                return [];
              }),
              catchError(err => from([
                new HideImportSpinner()
              ]))
            );
        })
      );
    
      @Effect()
      fetchImportStatus: Observable<Action> = this.actions$.pipe(
        ofType<FetchImportStatus>(ImportConfigActionTypes.FetchImportStatus),
        switchMap((action) => {
          return this.service.fetchImportStatus(action.projectId, action.importId)
            .pipe(
              mergeMap((res: any) => {
                  if (res.status === 'Success') {
                    return [
                      new FetchImportStatusSuccess(res.data)
                    ];
                  }
              }),
              catchError(err => from([
                new FetchImportStatusFailure()
              ]))
            );
        })
      );
    
      @Effect()
      fetchImportProgress: Observable<Action> = this.actions$.pipe(
        ofType<FetchImportProgress>(ImportConfigActionTypes.FetchImportProgress),
        switchMap((action) => {
          return this.service.fetchImportProgress(action.projectId, action.importId)
            .pipe(
              mergeMap((res: any) => {
                if (res.status === 'Success') {
                  return [
                    new FetchImportProgressSuccess(res.data)
                  ];
                }
              }),
              catchError(err => from([
                new FetchImportProgressFailure()
              ]))
            );
        })
      );
    }

Here're my selectors:

    import {createSelector} from '@ngrx/store';
    import {ImportConfig} from '../../models/import-config';
    import {ImportConfigState} from '../reducers/import-config.reducer';
    import {getImportState, ImportState} from '../reducers';
    
    export const getImportConfigState = createSelector(
      getImportState,
      (importState: ImportState) => importState.importConfig
    );
    
    export const getImportConfig = createSelector(
      getImportConfigState,
      (importConfigState: ImportConfigState) => importConfigState.importConfig
    );
    
    export const isImportSpinnerShowing = createSelector(
      getImportConfigState,
      (importConfigState: ImportConfigState) => importConfigState.importSpinner
    );
    
    export const getImportStatus = createSelector(
      getImportConfigState,
      (importConfigState: ImportConfigState) => importConfigState.importStatus
    );
    export const getImportProgress = createSelector(
      getImportConfigState,
      (importConfigState: ImportConfigState) => importConfigState.importProgress
    );

Here's my component:

    import {Component, OnDestroy, OnInit, ViewEncapsulation} from '@angular/core';
    import {select, Store} from '@ngrx/store';
    import {ImportState} from '../../store/reducers';
    import {library} from '@fortawesome/fontawesome-svg-core';
    import {faAngleLeft, faAngleRight, faExchangeAlt,
      faFolder, faFolderOpen, faFileImport, faLink, faEquals, faCogs,
      faExclamationCircle, faFilter, faSearch, faHome} from '@fortawesome/free-solid-svg-icons';
    import {faFile} from '@fortawesome/free-regular-svg-icons';
    import {FetchImportProgress, FetchImportStatus} from '../../store/actions';
    import {ActivatedRoute} from '@angular/router';
    import {Subject} from 'rxjs';
    import {BsModalRef, BsModalService} from 'ngx-bootstrap';
    import {ImportProgressComponent} from '../import-progress/import-progress.component';
    import {getImportStatus} from '../../store/selectors';
    import {filter, map, takeUntil} from 'rxjs/operators';
    import {ImportStatus} from '../../models/import-status';
    
    @Component({
      selector: 'app-import',
      templateUrl: './import.component.html',
      styleUrls: ['./import.component.scss'],
      encapsulation: ViewEncapsulation.None
    })
    export class ImportComponent implements OnInit, OnDestroy {
    
      importId: string;
      projectId: string;
    
      status: number;
      phase: number;
    
      private importProgressModalRef: BsModalRef;
      private isProgressModalShowing = false;
    
      private unsubscribe$ = new Subject<void>();
    
      queryParamsSubscription: any;
    
      constructor(
        private store: Store<ImportState>,
        private route: ActivatedRoute,
        private modalService: BsModalService) {
    
        library.add(
          faHome,
          faFolder, faFolderOpen, faFile, faFileImport,
          faAngleRight, faAngleLeft,
          faFilter, faSearch,
          faExchangeAlt,
          faLink,
          faEquals,
          faCogs,
          faExclamationCircle);
    
        this.queryParamsSubscription = this.route.queryParams
          .subscribe(params => {
            this.importId = params['importId'];
            this.projectId = params['projectId'];
          });
      }
    
      ngOnInit(): void {
        this.store.dispatch(new FetchImportStatus(+this.projectId, +this.importId));
        this.store.dispatch(new FetchImportProgress(+this.projectId, +this.importId));
    
        this.store.pipe(select(getImportStatus), takeUntil(this.unsubscribe$), map((importStatus: ImportStatus) => importStatus),
          filter((importStatus: ImportStatus) => !!importStatus))
          .subscribe((importStatus: ImportStatus) => {
            this.status = importStatus.status; // This is getting triggered for all property changes
            this.phase = importStatus.phase;
            this.handleStatusChange();
          });
      }
    
      ngOnDestroy(): void {
        this.unsubscribe$.next();
        this.unsubscribe$.complete();
    
        this.queryParamsSubscription.unsubscribe();
      }
      
      handleStatusChange() {
        if (this.status !== 2 || (this.phase === 5)) {
          if (!this.isProgressModalShowing) {
            this.openImportProgressModal();
            this.isProgressModalShowing = true;
          }
        }
      }
    
      openImportProgressModal() {
        this.importProgressModalRef = this.modalService.show(ImportProgressComponent,
          Object.assign({}, { class: 'modal-md', ignoreBackdropClick: true }));
        this.importProgressModalRef.content.modalRef = this.importProgressModalRef;
        this.importProgressModalRef.content.onModalCloseCallBack = this.onImportProgressModalClose;
      }
    
      onImportProgressModalClose = () => {
        this.isProgressModalShowing = false;
      };
    }

Jean Marc

unread,
May 13, 2019, 12:24:59 PM5/13/19
to Angular and AngularJS discussion
Hello, i already had this kind of pb's
I think you have to replace "mergeMap" => "switchMap" in yours effects.

Jean Marc

unread,
May 13, 2019, 12:24:59 PM5/13/19
to Angular and AngularJS discussion
Hello,
I think you have to replace "mergeMap" => "switchMap" in yours effects

Le dimanche 12 mai 2019 07:03:59 UTC+2, Sagun Pandey a écrit :

Sagun Pandey

unread,
May 13, 2019, 12:32:43 PM5/13/19
to Angular and AngularJS discussion
I am using switchMap. The mergeMap is inside the pipe operator for service's response observable. I am using that to dispatch multiple actions. This is working perfectly for all other properties.

Jean Marc

unread,
May 14, 2019, 3:52:04 AM5/14/19
to Angular and AngularJS discussion
what i'm saying it's you seem have effects linked to `getImportStatus` and `getImportProgress`, fetchImportStatus and fetchImportProgress which are the both properties where you have troubles . using a switchMap instead of mergeMap here ' .pipe(
              mergeMap((res: any) => {'XXX'  I think you have a problem of unsubscription which call is automatically done with a switchMap before rendering the observable(s) outputs. 

Sagun Pandey

unread,
May 15, 2019, 3:19:02 PM5/15/19
to ang...@googlegroups.com
Thank you for responding. Using the switchMap gave the same result :(

Regards,


On Tue, May 14, 2019 at 3:52 AM Jean Marc <celest...@gmail.com> wrote:
what i'm saying it's you seem have effects linked to `getImportStatus` and `getImportProgress`, fetchImportStatus and fetchImportProgress which are the both properties where you have troubles . using a switchMap instead of mergeMap here ' .pipe(
              mergeMap((res: any) => {'XXX'  I think you have a problem of unsubscription which call is automatically done with a switchMap before rendering the observable(s) outputs. 

--
You received this message because you are subscribed to the Google Groups "Angular and AngularJS discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to angular+u...@googlegroups.com.
To post to this group, send email to ang...@googlegroups.com.
Visit this group at https://groups.google.com/group/angular.
To view this discussion on the web visit https://groups.google.com/d/msgid/angular/d2209c3d-418f-4173-82e9-246a0795a6c6%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Reply all
Reply to author
Forward
0 new messages