Как я могу поймать два (или более) действия @ ngrx / store и удерживать подписку в компонентах, пока это не произойдет?

В моем основном компоненте я начинаю инициализацию своих данных:

_store.dispatch(_statusActions.initialize());

Это запускает все действия инициализации:

@Effect()
loading$ = this.actions$
    .ofType(StatusActions.INITIALIZING)
    .mergeMap(() => Observable.from([
        this._characterActions.initCharacters(),
        this._vehicleActions.initVehicles()
    ]))

После загрузки данных в Store для всех сущностей магазина срабатывают успешные действия.

Для автомобилей:

@Effect()
loadVehicles$ = this.actions$
    .ofType(VehicleActions.INIT_VEHICLES)
    .switchMap(() => this._vehicleService.getVehicles()
        .map((vehicles: Vehicle[]) => this._vehicleActions.initVehiclesSuccess(vehicles))
        .catch(err => Observable.of(this._statusActions.dataLoadingError('vehicles')))
    );

А для персонажей:

@Effect()
loadVehicles$ = this.actions$
    .ofType(CharacterActions.INIT_CHARACTERS)
    .switchMap(() => this._characterService.getCharacters())
    .map((characters: Character[]) => 
        this._characterActions.initCharactersSucess(characters))

И, наконец, после срабатывания всех действий * _DATA_SUCCESS, я хочу, чтобы мое действие INITIALIZED было запущено для установки флага READY в моем хранилище.

export const initReducer = (state: boolean = false, action: Action): boolean => {
switch (action.type){
    case StatusActions.INITIALIZING:
        return false;
    case StatusActions.INITIALIZED:
        console.log('App initialized...');
        return true;
    default: 
        return state;
}

Мой вопрос - как это сделать? Как узнать, когда сработали все успешные действия?

UPD

Snks mtx, я следил за вашим первым и быстрее adwise.

Извините за лишний вопрос, но я действительно застрял в поисках хорошего способа сделать следующий шаг. Как удерживать эту подписку (внутри компонента) до тех пор, пока мое ИНИЦИАЛИЗИРОВАННОЕ действие не будет запущено (нужно удалить этот ужасный костыль с помощью if (cars.length> 0)):

constructor(
...
) {
  this.vehicles$ = _store.select(s => s.vehicles);
  this.initialized$ = _store.select(s => s.initilized);
}

ngOnInit() {
let id = this._route.snapshot.params.id ? this._route.snapshot.params.id : 
null;
this.sub = this.vehicles$.subscribe(vehicles => {
    if (vehicles.length > 0){
      if(id){
        this.vehicle = vehicles.find(item => item.id === Number(id))     
      } else {
        this.vehicle = new Vehicle(vehicles[vehicles.length-1].id+1, '');
      }
      this.viewReady = true;
    }
  })
}  

ngOnDestroy(){
  this.sub && this.sub.unsubscribe();
}

Я попытался вставить skipUntil () перед подпиской (), но теперь у меня возникают проблемы с открытием этого компонента из другого (когда все данные уже загружены). В этом случае обратный вызов подписки больше не может запускаться! Я не понимаю, почему ...

...
private initDone$ = new Subject<boolean>();
...

this.initialized$.subscribe((init: Init) => {
  if(init.app) 
    this.initDone$.next(true);
})

this.sub = this.vehicles$.skipUntil(this.initDone$).subscribe(vehicles => {
    if(id)
      this.vehicle = vehicles.find(item => item.id === Number(id))     
    else 
      this.vehicle = new Vehicle(vehicles[vehicles.length-1].id+1, '');

    this.viewReady = true;
  });  
}  

Чтобы воспроизвести мою проблему, просто нажмите на один из автомобилей в списке. Обратный вызов подписки не срабатывает. Затем нажмите F5 -> теперь машина загружается, обратный вызов маяка был запущен, как и было задумано.

Полный исходный код находится здесь: GitHub, последняя версия работает на страницах GitHub


person Anton Pegov    schedule 06.06.2017    source источник


Ответы (1)


Я могу придумать два способа сделать это (я уверен, что есть и другие способы):

1. В редукторе

Пусть состояние initReducer имеет флаги для каждого запроса, который должен быть успешным, чтобы флаг готовности был установлен в true, уменьшив оба действия * _DATA_SUCCESS в initReducer:

init.reducer.ts

export interface InitState = {
    characterSuccess: boolean,
    vehicleSuccess: boolean,
    ready: boolean
}

const initialState = {
    characterSuccess = false,
    vehicleSuccess = false,
    ready = false
};

export const initReducer (state: InitState = initialState, action: Action): InitState {
    switch (action.type) {
        /* ...
         * other cases, like INITIALIZING or INITIALIZING_ERROR...
         */

        case CharacterActions.CHARACTER_DATA_SUCCESS: {
            /* set characterSuccess to true.
             *
             * if vehicleSuccess is already true
             * this means that both requests completed successfully
             * otherwise ready will stay false until the second request completes
             */
            return Object.assign({}, state, {
                characterSuccess: true
                ready: state.vehicleSuccess
            });
        }

        case VehicleActions.VEHICLE_DATA_SUCCESS: {
            /* set vehicleSuccess to true.
             *
             * if characterSuccess is already true
             * this means that both requests completed successfully
             * otherwise ready will stay false until the second request completes
             */
            return Object.assign({}, state, {
                vehicleSuccess: true,
                ready: state.characterSuccess
            });
        }

        default:
            return state;
    }
}

2. Используйте селекторы

Если вы создали initReducer только для того, чтобы отслеживать, инициализируете ли вы в данный момент, вы можете опустить весь редуктор и использовать вместо него селекторы, которые вычисляют производное состояние.
Мне нравится использовать Reselect, потому что она позволяет создавать эффективные селекторы, которые пересчитываются только тогда, когда происходит изменение (= запомненные селекторы).

Сначала добавьте loading- и ready-flag к форме состояния как для транспортных средств, так и для символов-редукторов.
Затем добавьте функции-селекторы на уровне редуктора.
Пример для VehiclesReducer:

vehicle.reducer.ts (повторите то же самое с символами-редуктором)

export interface VehicleState {
    // vehicle ids and entities etc...
    loading: boolean;
    ready: boolean;
}

const initialState: VehicleState = {
    // other init values
    loading: false,
    ready: false
}

export function reducer(state = initialState, action: Action): VehicleState {
    switch (action.type) {
        // other cases...

        case VehicleActions.INIT_VEHICLES: {
            return Object.assign({}, state, {
                loading: true,
                ready: false
            });
        }

        case VehicleActions.VEHICLE_DATA_SUCCESS: {
            return Object.assign({}, state, {
                /* other reducer logic like
                 * entities: action.payload
                 */
                loading: false,
                ready: true
            });
        }

        default:
            return state;
    }
}

// Selector functions
export const getLoading = (state: VehicleState) => state.loading;
export const getReady = (state: VehicleState) => state.ready;

Затем в вашем корневом редукторе или в дополнительном файле, куда вы помещаете свои селекторы, составьте селекторы, которые дадут вам желаемое производное состояние:

selectors.ts

import { MyGlobalAppState } from './root.reducer';
import * as fromVehicle from './vehicle.reducer';
import * as fromCharacter from './character.reducer';
import { createSelector } from 'reselect';

// selector for vehicle-state
export const getVehicleState  = (state: MyGlobalAppState) => state.vehicle;
// selector for character-state
export const getCharacterState = (state: MyGlobalAppState) => state.character;

// selectors from vehicle
export const getVehicleLoading = createSelector(getVehicleState, fromVehicle.getLoading);
export const getVehicleReady = createSelector(getVehicleState, fromVehicle.getReady);

// selectors from character
export const getCharacterLoading = createSelector(getCharacterState, fromCharacter.getLoading);
export const getCharacterReady = createSelector(getCharacterState, fromCharacter.getReady);

// combined selectors that will calculate a derived state from both vehicle-state and character-state
export const getLoading = createSelector(getVehicleLoading, getCharacterLoading, (vehicle, character) => {
    return (vehicle || character);
});

export const getReady = createSelector(getVehicleReady, getCharacterReady, (vehicle, character) => {
    return (vehicle && character);
});

Теперь вы можете использовать этот селектор в своем компоненте:

import * as selectors from './selectors';

let loading$ = this.store.select(selectors.getLoading);
let ready$ = this.store.select(selectors.getReady);

loading$.subscribe(loading => console.log(loading)); // will emit true when requests are still running
ready$.subscribe(ready => console.log(ready)); // will emit true when both requests where successful

Хотя этот подход может быть более подробным, он более понятен и соответствует установленным методам сокращения. И вы могли бы опустить все initReducer.

Если вы раньше не использовали селекторы, они представлены в ngrx-example-app. .


По поводу обновления:

Так как вы используете маршрутизатор, вы можете, например, использовать защиту маршрутизатора, чтобы приостановить активацию маршрута до завершения инициализации. Реализуйте интерфейс CanActivate:

@Injectable()
export class InitializedGuard implements CanActivate {
    constructor(private store: Store<MyGlobalAppState>) { }

    canActivate(): Observable<boolean> {
        return this.store.select(fromRoot.getInitState) // select initialized from store
            .filter(initialized => initialized === true)
            .take(1)
    }
}

Затем добавьте охранника к своему маршруту:

{
    path: 'vehicle/:id',
    component: VehicleComponent,
    canActivate: [ InitializedGuard ]
}

Охранники активации маршрута также представлены в приложении ngrx-example-app, посмотрите здесь

person mtx    schedule 06.06.2017
comment
Санки много! Работает отлично. Не могли бы вы прочитать мое обновление и сказать, что вы думаете. - person Anton Pegov; 07.06.2017
comment
@AntonPegov Я обновил свой ответ, чтобы ответить на ваш дополнительный вопрос. Но я рекомендую вам задать новый вопрос, потому что это может стать довольно сложным вопросом (это не по теме для этого вопроса). Например, если вы следуете шаблону «умный / тупой» компонент, возможно, вообще не потребуется иметь защиту активации, потому что правильно размещенный *ngIf=(initialized$ | async) может быть всем, что вам нужно сделать в вашем компоненте. Но, как я уже сказал, это определенно оправдывает собственный вопрос ... :) - person mtx; 07.06.2017
comment
Снова Санкс! Вы так добры. Надеюсь, я смогу сохранить все в одном посте, потому что это действительно один простой вариант использования. В основном о том, как инициализировать ваши данные, прежде чем показывать компонент, который их использует. - person Anton Pegov; 07.06.2017
comment
Хорошо, я рад, что смог помочь :) - person mtx; 07.06.2017