Angular 7 с NGRX - подписка на свойство магазина через селектор, вызываемый несколько раз - PullRequest
0 голосов
/ 11 мая 2019

Я испытываю странное поведение с моим приложением.Я подписался на свойство состояния с помощью селектора, и я вижу, что моя подписка вызывается независимо от того, какое свойство в состоянии изменяется.

Ниже приведена очищенная версия моего кода.Мое состояние имеет все виды свойств, некоторые объекты и некоторые плоские свойства.Селекторы для всех свойств работают как положено, кроме селекторов getImportStatus и getImportProgress. Подписка на эти селекторы активируется независимо от того, какое свойство в магазине меняется. Я просто схожу с ума.Кто-нибудь может подсказать, что я делаю не так?Кто-нибудь сталкивался с такой проблемой?Я знаю, что люди сталкиваются с подобными проблемами, когда они не отписываются от подписок.Но в моем случае, как вы можете видеть, я отписался, и событие вызывается для любого изменения свойства, которое меня озадачило.

Вот мой редуктор:

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 голосов
/ 13 мая 2019

Я не мог понять, что происходит. Поскольку у меня не хватало времени, мне пришлось пойти на альтернативный взлом.

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, чтобы сравнить новое свойство store со старым в моем теле подписки. Это не нужно, поскольку хранилище должно было отправлять только измененные значения. Пока, по крайней мере, это продолжит меня.

НОВОЕ ОБНОВЛЕНИЕ

Причина, по которой мои подписки на хранилище свойств вызывались несколько раз, заключалась в том, что состояние не было полностью клонировано. Я использовал функцию _cloneDeep, предоставленную lodash, для глубокого клонирования моего состояния и обновления свойств. Я полагаю, что ни одна библиотека не эффективна на 100%, когда дело доходит до клонирования.

...