Search code examples
angularrxjsngrx

ngrx sequential data loading


I have a guard, where I want to ensure the data is pre-loaded before the page visually loads. I've been following Todd Motto's strategy, for pre-loading data that only requires one dispatch.

The issue is, my scenario requires multiple dispatches, where some are dependent on others.

I need to dispatch to load object A, then I need to use some data on object A to load object B.

For the sake of this minimal example, assume my state has loading (boolean), and things[] (loaded entities). When I do a Get action, loading is flipped to true. loading gets flipped to false on an unseen success action.

canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
    // fetches the first `thing` with the route's id
    this.store.dispatch(new actions.Get(route.params.id));
    return this.store
        .select(selectState)
        .filter(state => !!!state.loading)
        .do(state => {
            // the first `thing` is loaded
            // now use it to load another `thing`
            this.store.dispatch(new actions.Get(state.things[0].something));
        })
        .take(1)
        .switchMap(() => this.store.select(selectState))
        // in the filter, state.loading is false, even though
        // we just fired a dispatch to say it's loading. 
        // ...because this is checked before the
        // dispatch can flip the boolean
        .filter(state => !!!state.loading)
        .take(1)
        .switchMap(() => Observable.of(true))
        .catch(() => Observable.of(false));
}

While everything works fine for the first "wait for this to load", the subsequent dispatches don't flip my loading state variable to true before the next "wait for this to load" check.

Any help, or maybe a better pattern, is greatly appreciated.


Solution

  • Let's say my state looks like this

    {
      user: User;
      userLoaded: boolean;
      userRoles: Role;
      userRolesLoaded: boolean
    }
    

    My goal is to not load a route until both the user and the userRoles are loaded.

    Let's assume I have created selectors for getUserLoaded, and getUserRolesLoaded that only return the boolean values from my state for their respective properties.

    Here is my approach to load multiple

    canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
      return this.store.select(getUserLoaded).pipe(
          map(loaded => {
            // if my user is loaded this will be true
            if (!loaded) {
              this.store.dispatch(new fromStore.LoadUser());
            }
          }),
          filter(loaded) => loaded),
          take(1),
          withLatestFrom(this.store.select(fromStore.getUserRolesLoaded)),
          map(([nullData, loaded]) => {
            if(!loaded) {
              this.store.dispatch(new fromStore.LoadUserRoles()
            }
          }),
          withLatestFrom(this.store.select(fromStore.getUserRolesLoaded)),
          filter(([data, loaded]) => loaded),
          take(1)
        );
    }
    

    So here I first get the slice of state that is my userLoaded boolean. If my user is not loaded then I dispatch the action that will load the user. Then I filter it so that it won't return until true and take one to unsubscribe. Next I get the slice of state that would have getUserRolesLoaded and I check if they are loaded if not I dispatch, add a filter wait for true and take 1.

    I am using the pipe method introduced in rxjs 5.5 but converting it for your needs shouldn't be too hard. For me I feel the key was understanding what slice of state I was selecting to ensure I was checking for the appropriately loaded data.