How to attach ngrx/store with an angular router guard

Marco Rinck picture Marco Rinck · Feb 28, 2017 · Viewed 9.4k times · Source

I'm a beginner with ngrx/store and this is my first project using it.

I have successfully set up my angular project with ngrx/store and I'm able to dispatch a load action after initializing my main component like this:

ngOnInit() { this.store.dispatch({type: LOAD_STATISTICS}); }

I have set up an effect to load the data when this action is dispatched:

@Effect()
loadStatistik = this.actions.ofType(LOAD_STATISTICS).switchMap(() => {
    return this.myService.loadStatistics().map(response => {
        return {type: NEW_STATISTICS, payload: response};
    });
});

My reducer looks like this:

reduce(oldstate: Statistics = initialStatistics, action: Action): Statistik {
    switch (action.type) {
        case LOAD_STATISTICS:
            return oldstate;
        case NEW_STATISTICS:
            const newstate = new Statistics(action.payload);

            return newstate;
    ....

Although this works, I can't get my head around how to use this with a router guard as I currently need to dispatch the LOAD_ACTION only once.

Also, that a component has to dispatch a LOAD action, to load initial data doesn't sound right to me. I'd expect that the store itself knows that it needs to load data and I don't have to dispatch an action first. If this were the case, I could delete the ngOnInit() method in my component.

I already have looked into the ngrx-example-app but I haven't understood really how this works.

EDIT:

After adding a resolve guard that returns the ngrx-store observable the route does not get activated. Here is the resolve:

   @Injectable()
  export class StatisticsResolver implements Resolve<Statistik> {
    resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Statistik> {
        // return Observable.of(new Statistik());
        return this.store.select("statistik").map(statistik => {
        console.log("stats", statistik);

        return statistik;
    });
}

This is the route:

const routes: Routes = [
    {path: '', component: TelefonanlageComponent, resolve: {statistik:  TelefonieStatistikResolver}},
];

Answer

notANerdDev picture notANerdDev · Apr 1, 2017

I don't quite understand why you would send the resolved data to your component through the resolver. The whole point of setting up a ngrx store is to have a single source of data. So, what you simply want to do here, is make sure that the data is true. Rest, the component can get the data from the store using a selector.

I can see that you are calling LOAD_ACTION from the ngOnInit method of your component. You cannot call an action to resolve data, which then leads to the component, on Init of which you call the action! This is why your router isn't loading the route.

Not sure if you understood that, but it makes sense!

In simple terms, what you are doing is this. You are locking a room and then pushing the key through the gap beneath the door, and then wondering why the door isn't opening.

Keep the key with you!

The guard's resolve method should call the LOAD_ACTION. Then the resolve should wait for the data to load, and then the router should proceed to the component.

How do you do that?

The other answers are setting up subscriptions, but the thing is you don't want to do that in your guards. It's not good practice to subscribe in guards, as they will then need to be unsubscribed, but if the data isn't resolved, or the guards return false, then the router never gets to unsubscribe and we have a mess.

So, use take(n). This operator will take n values from the subscription, and then automatically kill the subscription.

Also, in your actions, you will need LOAD_STATISTICS_SUCCESS and a LOAD_STATISTICS_FAIL. As your service method can fail!

In the reducer State, you would need a loaded property, which turns to true when the service call is successful and LOAD_STATISTICS_SUCCESS action is called.

Add a selector in the main reducer, getStatisticsLoaded and then in your gaurd, your setup would look like this:

resolve(): Observable<boolean> {

this.store.dispatch({type: LOAD_STATISTICS});

return this.store.select(getStatisticsLoaded)
    .filter(loaded => loaded)
    .take(1);

}

So, only when the loaded property changes to true, filter will allow the stream to continue. take will take the first value and pass along. At which point the resolve completes, and the router can proceed.

Again, take will kill the subscription.