Mike
Mike

Reputation: 4091

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.

Upvotes: 0

Views: 2522

Answers (3)

David Taylor
David Taylor

Reputation: 1

I was struggling to see why the code behaved as it did, so in case anyone else is wondering:

The @ngrx/store State class creates an Observable actionsOnQueue using the rxjs observeOn operator and the queueScheduler. It is these queued actions which are fed to the reducer to produce the new state. The new state then emits and triggers any selector.

The first dispatch executes in the context of the Resolver and runs synchronously. The next line creates an Observable which selects from the store and immediately sees the effect of the Get action. Thus the filter ignores the initial state where loading=true and the pipeline waits for the loading to complete.

When the Success action is later dispatched and reduced (setting loading=false), the store will emit and the filter will pass. Then the second dispatch will execute. The queueScheduler means this dispatch will not execute synchronously, as we are executing 'inside' the dispatch of the Success action.

The switchMap will then subscribe again to the store before the second Get action has been reduced into the state and see the stale state with loading=false.

Upvotes: 0

Woot
Woot

Reputation: 1809

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.

Upvotes: 2

Fan Cheung
Fan Cheung

Reputation: 11380

Maybe try this quick hack

.take(1)
    .switchMap(() => this.store.select(selectState).pluck('loading').distingUntilChanged()
)

which only fire if it's switch from false to true, or vice versa use. Alternatively use intermediate state, beforeload->loading->loaded

Upvotes: 0

Related Questions