DEV Community

Discussion on: Where to initiate data load in NgRx

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