In my main component I starting initialization of my data:
_store.dispatch(_statusActions.initialize());
That triggers all initialization actions:
@Effect()
loading$ = this.actions$
.ofType(StatusActions.INITIALIZING)
.mergeMap(() => Observable.from([
this._characterActions.initCharacters(),
this._vehicleActions.initVehicles()
]))
After data is loaded in the Store, success actions triggeres for all of store entities.
For vehicles:
@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')))
);
And for characters:
@Effect()
loadVehicles$ = this.actions$
.ofType(CharacterActions.INIT_CHARACTERS)
.switchMap(() => this._characterService.getCharacters())
.map((characters: Character[]) =>
this._characterActions.initCharactersSucess(characters))
And, finally, after all *_DATA_SUCCESS actions triggered, I want my INITIALIZED action to be triggered to put READY flag in my Storage.
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;
}
My question - how to perfom that? How to know when all success actions has been triggered?
UPD
Snks mtx, I followed your first and faster adwise.
Sory for extra question, but I realy stuck looking for nice whay to do the next step. How to hold this subscription (inside component) till my INITIALIZED action will be fired (need to remove this terrible crutch with if(vehicles.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();
}
I tried to insert skipUntil() before subscribe(), but now I get problem opening this component from another one (when all data already loaded). In this case subscribe callback can't fired any more! I can't understend why...
...
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;
});
}
To reproduce my problem just press on one of the vehicles in list. Subscribe callback not firing. Then press F5 -> now vehicle loading, beacouse callback was fired as designed.
Full source code is here: GitHub, last version running on GitHub Pages
I can think of two ways to do this (I'm sure there are other ways):
Let the initReducer
-state have flags for each request that has to succeed for the ready-flag to be set to true
by reducing both *_DATA_SUCCESS actions in the 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;
}
}
If you created the initReducer
just to track if you are currently initializing, you can omit the whole reducer and use selectors that compute a derived state instead.
I like to use the reselect library because it lets you create efficient selectors that only recompute when a change happens (= memoized selectors).
First, add a loading
- and ready
-flag to the state-shape of both the Vehicles- and the Characters-reducer.
Then, add selector-functions at the reducer level.
Example for VehiclesReducer:
vehicle.reducer.ts (Repeat the same for the Characters-reducer)
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;
Next, in your root-reducer or in an additional file where you put your selectors, compose selectors that give you the desired derived state:
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);
});
Now you can use these selector in your component:
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
While this approach may be more verbose, it is cleaner and follows established practices of redux. And you might be able to omit the whole initReducer
.
If you haven't used selectors before, it is showcased in the ngrx-example-app.
Regarding the update:
Since you are using the router, you could for example use a router-guard to withhold route activation until initializing is done. Implement the CanActivate interface:
@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)
}
}
Then, add the guard to your route:
{
path: 'vehicle/:id',
component: VehicleComponent,
canActivate: [ InitializedGuard ]
}
Route-activation guards are also showcased in the ngrx-example-app, look here