Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to attach ngrx/store with an angular router guard

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()
loadStatistic = 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): Statistic {
    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<Statistic> {
    resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Statistic> {
        // return Observable.of(new Statistic());
        return this.store.select("statistic").map(statistic => {
        console.log("stats", statistic);

        return statistic;
    });
}

This is the route:

const routes: Routes = [
    {path: '', component: TelefonanlageComponent, resolve: {statistic:  TelefonieStatisticResolver}},
];
like image 274
Marco Rinck Avatar asked Feb 28 '17 11:02

Marco Rinck


People also ask

What is NgRx router store?

@ngrx/router-store keeps router state up-to-date in the store and uses the store as the single source of truth for the router's state.

Where does NgRx store its data?

Where Does NgRx Store Data? NgRx stores the application state in an RxJS observable inside an Angular service called Store. At the same time, this service implements the Observable interface.

Is NgRx store an observable?

The NgRX Store imports the state management concepts from Redux and adds to them RxJS to provide an observable means of communication throughout the Store APIs, thus giving Angular developers a familiar experience in developing Angular apps.


2 Answers

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.

like image 85
notANerdDev Avatar answered Sep 18 '22 15:09

notANerdDev


I just solved it myself after valuable input from AngularFrance. As I'm still a beginner, I don't know if this is how its supposed to be done, but it works.

I implemented a CanActivate Guard like this:

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

    waitForDataToLoad(): Observable<Statistic> {
        return this.store.select(state => state.statistic)
            .filter(statistic => statistic && !statistic.empty);
    }

    canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
        this.store.dispatch({type: LOAD_STATISTIC});

        return this.waitForDataToLoad()
            .switchMap(() => {
                return Observable.of(true);
            });
        }
    }
}

The method canActivate(...) is first dispatching an action to load the data. In waitForDataToLoad() we filter that the data is already there and not empty (an implementation detail of my business logic).

After this returns true, we call switchMap to return an Observable of true.

like image 29
Marco Rinck Avatar answered Sep 17 '22 15:09

Marco Rinck