简体   繁体   中英

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}},
];

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.

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.

I'm assuming by "router guard" you mean a resolve guard? As in you'd like for the data to be loaded before activating the route?

If yes, then everything should play well together:

  • Values from ngrx/store are exposed as observables (eg in their docs store.select('counter') contains an observable).
  • In Angular resolve guards can return observables .

So you could simply return the ngrx/store observable from your resolve:

class MyResolver implements Resolve<any> {

  constructor(private store: Store<any>) {}

  resolve(): Observable<any> {
    // Adapt code to your situation...
    return this.store.select('statistics');
  }
}

That being said your setup does seem a little complex. I tend to think of state as transient data (collapsed state of a menu, data that needs to be shared between multiple screens/components for the duration of the session such as the current user) but I'm not sure I'd load a big slice of data from the backend into the state.

What do you gain from storing the statistics in the state? (vs. simply loading them and displaying them in some component)

If loading is not successfull then probably you want to cancel navigation. It can be done by throwing error. Most important thing is - returned observable must be completed or finished with error.

resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
): Observable<boolean> {

    this.store.dispatch(new LoadStatistic(route.params.someParam));

    return this.store.pipe(select(selectStatisticModel),
        filter(s => !s.isLoading),
        take(1),
        map(s => {
            if (!s.isLoadSuccess) {
                throw s.error;
            }
            return true;
        })
    );
}

This also considers a failed request:

canActivate(): Observable<boolean> {
    this.store.dispatch({type: LOAD_STATISTICS);

    return this.store.select((state) => state.statistics)
      .filter((statistics) => statistics.loaded || statistics.loadingFailed)
      .map((statistics) => statistics.loaded);
  }

This answer for people who get frustrated using ngrx/effect in this situation. Maybe better to use just simple approach without ngrx/effects.

canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
    // this will only dispatch actions to maybe clean up, NO EFFECTS 
    this.store.dispatch({type: LOAD_STATISTIK});     

  return this.service.loadOne()
     .do((data) => {
         this.store.dispatch({type: LoadSuccess, payload: data })
      }).map(() => {
      return true; 
    })  
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM