DEV Community

Where to initiate data load in NgRx

Jon Rimmer on January 22, 2019

In NgRx, loading data from a data source, such as a REST API or a DB, is done using an effect. However, something has to dispatch the action that t...
Collapse
 
timdeschryver profile image
Tim Deschryver

NgRx 7 brings us some new Effect lifecycle hooks.
One of them, OnInitEffects, might be useful to initiate a data load when an effect gets registered - see docs.

class OrdersEffects implements OnInitEffects {
  ngrxOnInitEffects(): Action {
    return { type: '[Orders Effect]: Load' };
  }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
sebasg22 profile image
Sebastián Guevara

Great post Jon, There was two ways that works for me to dispatch an action when the app starts:

@Effect()
  $init = of(new ListenAuth, asyncScheduler);
Enter fullscreen mode Exit fullscreen mode

But as Tim Descrhryver saids onInit Effects also works if you use NgRx7:

ngrxOnInitEffects(): Action {
    return { type: '[Login]: Listen Auth' };
  }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
atrifyllis profile image
alx tr • Edited

What about dispatching action in APP_INITIALIZER? Or a Resolver?

Collapse
 
maxart2501 profile image
Massimo Artizzu • Edited

I was about to say the same thing. Actually, dispatching on a resolver is one of the most common things I do in an Angular+NgRx project.

Nice recap anyway.

Collapse
 
jonrimmer profile image
Jon Rimmer

Good point about APP_INITIALIZER. I did consider Resolver, but I figured it was similar enough to Guard. I'll update the article with both approaches.

Collapse
 
shevadavid profile image
David

I like an idea to dispatch action in selector, because this is the place where you actually know, do you have the data you need. However when I implemented a feature this way, I faced with strange behavior of NGRX\RxJS because in some point of time I receive initial state in the selector instead of updated. Maybe you know how to address this problem?

Collapse
 
wheeleruniverse profile image
Justin Wheeler • Edited

What a great post! I often struggle with the best approach to load the data. For small apps I like the idea of loading the data in the App Component, but I don't like the idea of loading data that may never be used. For example, if a lazy loaded feature is never interacted with by the user after the data is eagerly loaded.

In my own project I've created a Facade to abstract the NgRx layer from the rest of the Angular code. In there I am dispatching the Load action if the selector does not have the data loaded. This worked best for me, yet I still feel like it violates the NgRx best practices.

At least with this approach I don't need to worry about dispatching a load from my components. Just retrieve() the data and it will pull it from the service or the store.

  retrieve(): Observable<CertificationState> {
    return this.store.select(certificationStateSelector).pipe(
      tap((state) => {
        if (state?.data == null) {
          this.store.dispatch(new CertificationRetrieveAction());
        }
      })
    );
  }
Enter fullscreen mode Exit fullscreen mode

certification.facade.ts

Collapse
 
michaelkariv profile image
Michael Kariv

One tiny bit to add, hope this use case is not unique to me. Some data is a cache that does not need to update all that often. Such data is best loaded in the component or router. That is because 1. some of it might have been persisted to localstorage or forage and hydrated into the state 2. some of it might not be stale yet (e.g. my use case I want to invalidate at most once an hour) 3. Critically I want to provide the user the choice to refresh (i.e fetch the latest) - in the same page (smart component) where this info is to be displayed. It means the cache validation will happen onInit, and forceful update on a button click.

Collapse
 
matias_e_romero profile image
Matias Romero

Thank you Jon, nice post!
What should I do if I have a lot of complex entities (customer > address > country) and I don't want to load all of them in the state at once?

Basically, I need to "fetch" entities on selectors but I don't want to break the pattern.

Anyway, great work!

Collapse
 
jonrimmer profile image
Jon Rimmer

This is a good question, and it's one I've struggled with myself, and I'm not sure there is a perfect (or even a good) answer for NgRx / Redux as it stands. Usually what I end up doing is having the component(s) interested in a particular entity dispatch a "load" action on ngInit, and then have an effect on this action that checks whether the entity is already present in the store, and then loads it from the back-end if its not. You can mirror this with an "unload" action when the component is destroyed which clears the entity out of the store, so reducing the amount of data you're storing.

However, I have often thought there is room in NgRx for something like a reference-counted selector, where the first subscription to a selector dispatches a "load" event, which can load a resource into the store, and where the subscriber count going back down to zero dispatches an "unload" event. RxJS already has capabilities for performing reference counting on shared observables, so you might be able to use this to create smart selectors in this fashion. I hope to experiment with doing this myself some time, but I haven't yet got around it to it.

Collapse
 
amzhang profile image
Alan Zhang

A bit late to the party. But Jon's post is still at the top of google searches so obviously still quite relevant. We've faced the same issues and also the need to display loading states at a component level. We've elected to go with loading at the component level because it's the most fine grained approach.

The most important consideration is that the loading states should be tied to the ngrx action, not the data it actually loads because different actions could load overlapping pieces of data.

The biggest pain it solves for use is we can issue loading actions without needing to think about whether the data has already been loaded, i.e. a well behaved cache.

It sits strictly on-top of ngrx, no hidden magic.

A typical component looks like:

ngOnInit(): void {
  this.store.dispatch(Actions.fetchRecord.load({ id: this.recordId, maxAge: 5000 }));
  this.fetchRecordState$ = this.store.select(Selectors.selectFetchRecordState);
}
Enter fullscreen mode Exit fullscreen mode

The maxAge tells us it's ok to not load if the last load is less than 5000ms old. @michaelkariv you might be interested in this.

Template can display the loading state as such:

<div *ngIf="(fetchRecordState$ | async).loading">
  Loading...
</div>
Enter fullscreen mode Exit fullscreen mode

The fetchRecordState contains a much of other states related to the API call:

{
  loading: boolean; // Api is loading
  success: boolean; // Api returned successfully
  issueFetch: boolean; // true if we should issue a fetch
  errorHandler: LoadingErrorHandler;
  successTimestamp?: number; // Millisecond unix timestamp of when data is loaded. Date.now()
  error?: LrError2; // Api returned error
}
Enter fullscreen mode Exit fullscreen mode

Loading actions are bundled to reduce boiler plate:

export const fetchRecord = createLoadingActions(
  `[${NAMESPACE}] Fetch Record`,
  load<{ recordId: string }>(),
  success<{ record: Record }>()
  // failure action is automatically created.
);
Enter fullscreen mode Exit fullscreen mode

Effects handles the maxAge caching behaviour:

  fetchRecord$ = createEffect(() => {
    return this.actions$.pipe(
      ofType(VaultActions.fetchRecord.load),
      // This handles the maxAge caching behaviour
      LoadingStatesManager.filterLoading(this.store.select(fetchRecordSelectors.state)),
      switchMap((action) => {
        return this.vaultService.fetchRecord(action.recordId).pipe(
          map((records) => {
            return VaultActions.fetchRecord.success({
              record
            });
          }),
          VaultActions.fetchRecord.catchError()
        );
      })
    );
  });
Enter fullscreen mode Exit fullscreen mode

If there is interest, we'll put up an NPM package for it and we can explore togather.

Collapse
 
amzhang profile image
Alan Zhang
Collapse
 
yinon profile image
Yinon Oved

I was looking for a proper exhaust map implementation of such guard, good work!
but there's a valuable piece missing - the 'loading' state which is set to true at the request dispatch, so on the following dispatches it just leaves the loading state on true

Collapse
 
sebastiandg7 profile image
Sebastián Duque G

Thanks a lot for the post! Just a question: how would you handle loaders and errors when loading data in guards/resolvers?

Collapse
 
wesgrimes profile image
Wes

Fantastic. I somehow didn’t know about APP_INITIALIZER. I may use that.

Collapse
 
spock123 profile image
Lars Rye Jeppesen • Edited

A word of causion: if you use ROOT_EFFECTS_INIT you won't have access to the router selectors as they have not yet been initialized.

Collapse
 
iamdjarc profile image
IG:DjArc [Hey-R-C] • Edited

Hey Jon,

Thank you for this great article, I am a bit confused because of my set up, I hope you could help me understand.

In my app, I have couple modules which are using the StoreModule.forFeature flag so when I look for the "state" of those features I get a warning. telling that the state doesn't exist.

So I move the StoreModule.forFeature to the app module then it works. however the module for that feature doesn't anymore.

here is what my route looks like....

Also I am using the 'Route Guard
{
path: 'claim/:extClmId/client/:extClient/role/:roleType', canActivate: [ValidationGuard],
children: [
{ path: '', loadChildren: () => import('libs/lazy/firstview/src').then(m => m.FirstModule), canActivate: [OverviewGuard] },
{ path: 'preferences', loadChildren: () => import('@hub/lazy/preferences').then(m => m.SecondModule) },
],
},
{ path: 'error', loadChildren: () => import('@hub/lazy/four-oh-four').then(m => m.FourOhFourModule) },

OverviewGuard is what I am using to load my data but is it not the on the app.module level. it is on the FirstModule level..... Hope I am making sense.

Collapse
 
balajibalamurugan profile image
BalajiBalamurugan

what if we have multiple data to load ? what should we use ?