带有NGRX的Angular 7-通过选择器被多次调用来订阅商店属性

时间:2019-05-11 20:27:07

标签: angular rxjs ngrx ngrx-store ngrx-effects

我的应用程序遇到异常行为。我已经使用选择器订阅了一个state属性,而我看到的是,无论状态中的哪个属性发生变化,我的订阅都会被调用。

下面是我的代码的清理版本。我的状态具有各种属性,一些对象和一些平面属性。除getImportStatusgetImportProgress选择器外,所有属性的选择器均按预期工作。 无论商店中的什么属性发生变化,都将触发对这些选择器的订阅。我将失去理智。谁能暗示我在做什么错?有没有人遇到过这样的问题?我知道人们在不取消订阅时也会遇到类似的问题。但是,就我而言,如您所见,我正在取消订阅,并且此事件是由于使我感到困惑的任何财产变更而触发的。

这是我的减速器:

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;
  }
}

这是我的动作:

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;

这是我的特效:

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()
          ]))
        );
    })
  );
}

这是我的选择器:

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
);

这是我的组成部分:

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;
  };
}

1 个答案:

答案 0 :(得分:0)

我不知道发生了什么事。由于时间不多了,我不得不去找另一种方法。

this.store.pipe(select(getImportStatus), takeUntil(this.unsubscribe$), map((importStatus: ImportStatus) => importStatus),
  filter((importStatus: ImportStatus) => !!importStatus))
  .subscribe((importStatus: ImportStatus) => {
    if (_.isEqual(this.importStatus, importStatus)) {
      return;
    }
    this.importStatus = importStatus;
    this.status = importStatus.status;
    this.phase = importStatus.phase;
    this.handleStatusChange();
  });

我使用loadash库将新的商店属性与订阅主体内部的旧商店属性进行比较。这是不必要的,因为存储只应发出更改的值。现在,至少这会让我继续前进。