15

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}},
];
ofir_aghai
  • 3,017
  • 1
  • 37
  • 43
Marco Rinck
  • 738
  • 2
  • 6
  • 17

6 Answers6

13

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.

notANerdDev
  • 1,284
  • 11
  • 28
  • 1
    Thanks, perfect answer! The key point: **At which point the resolve completes, and the router can proceed. Again, take will kill the subscription.** – Roy Ling Nov 02 '17 at 06:39
  • @notANerdDev so in the component you get the data by the store not by the router data don't you ? btw very good replay :) – Whisher Dec 14 '17 at 21:46
  • 1
    @Whisher Yes. It's consistent with the purpose of using the store. – notANerdDev Dec 15 '17 at 03:00
8

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.

ofir_aghai
  • 3,017
  • 1
  • 37
  • 43
Marco Rinck
  • 738
  • 2
  • 6
  • 17
4

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 (e.g. 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)

AngularChef
  • 13,797
  • 8
  • 53
  • 69
  • Yes I meant resolve guards. However, when I just return the "select" Observable the route does not get activated at all. But I can subscribe to it and I receive data. Do I have to do something special to "resolve" it? PS: This is a simple demo project to learn about redux/ngrx, so the gain from storing the statistics is that I learn. – Marco Rinck Feb 28 '17 at 14:30
  • The router itself should subscribe to your observable. In the resolve try returning a dummy observable such as `return Observable.of("foo")` to find out whether it's the ngrx observable that's the problem, or if it's the way you declared the route with the resolve. – AngularChef Feb 28 '17 at 14:48
  • yes, that does work. I just edited my question with the resolver class. – Marco Rinck Feb 28 '17 at 14:58
  • So the dummy obs works but the ngrx one doesn't. When using the ngrx obs, do you see the output of the `console.log` you placed in your map()? Can you confirm there isn't another guard (e.g. on a parent route) preventing your route from activating? Are there no typos in your code? (your route references `TelefonieStatistikResolver` but the resolver you showed is called `StatisticsResolver`) – AngularChef Feb 28 '17 at 15:10
  • yes, I see the output of the log statement. There is no other guard present. If I change it to the dummy obs the route gets activated. The difference in spelling is that I tried to translate the original german names, but forgot to change it in every place. Its compiling and running just fine. – Marco Rinck Feb 28 '17 at 15:14
3

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;
        })
    );
}
Mārcis
  • 81
  • 2
  • 2
2

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);
  }
Martin Cremer
  • 5,191
  • 2
  • 32
  • 38
1

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; 
    })  
}
halfer
  • 19,824
  • 17
  • 99
  • 186
alexKhymenko
  • 5,450
  • 23
  • 40