angularrxjsngrxangular-resolver

How to proper implement an Angular Resolver using NGRX


I have been playing around with NGRX for the couple last days.

I'm following an Udemy course, and at some point in the course the teacher makes a resolver to prefetch some data into the ngrx store only in the first time we enter a given component.

Imagine something like this: load all books into the ngrx store, but only the first time we enter book-list-component, and only show the componenet when the store has really some books loaded.

The implementation on the course was pretty bad IMHO, using local variables, rxjs tap to make some side effects, and last but not the least the resolver didn't wait to the data being available in the store, it just loaded the component rigth away.

So I would like to show you my solution. I'm not really strong with Rxjs and that's where my doubts come from in this case.

export const booksResolver: ResolveFn<Observable<boolean>> = (): Observable<boolean> => {
    const bookStore: Store<BooksState> = inject(Store<BooksState>);

    return bookStore
        .select(BOOKS_SELECTORS.areBooksLoaded)
        .pipe(
            switchMap((value: boolean): Observable<boolean> => {
                if (!value) {
                    bookStore.dispatch(BOOKS_ACTIONS.loadBooks());
                }

                return bookStore
                    .select(BOOKS_SELECTORS.areBooksLoaded)
                    .pipe(
                        filter((value: boolean): boolean => value),
                    );
            }),
        );
};

Do you think, that for the purposes this is a realistic implementation? Is there some best practice to handle prefething global data into NGRX, other than using a resolver service?


Solution

  • I don't understand why, but there's a part of NgRx which is wildly ignored - RouterStore.

    You can use it to listen to router events and harness its neat selectors which can easily provide you with stuff you usually have to obtain from Router, ActivatedRouteSnapshot or by any other means.

    In your case, I would use the combination of Effect listening for routerNavigationAction and the simplified version of your resolver.

    // Effect
    prefetchBooks$ = createEffect(() => { 
      return this.action$.pipe(
        ofType(routerNavigationAction),
        // Listens to every navigation event => filter only route that you need
        filter(({ payload }) =>payload.routerState.url === 'myRoute'),
        // Dispatch action to preload data
        switchMap(() => bookStore.dispatch(BOOKS_ACTIONS.loadBooks())
    )});
    
    // Resolver
    // Action was already dispatched at this point, so just listen for data
    export const booksResolver: ResolveFn<Observable<boolean>> = (): Observable<boolean> => {
      const bookStore: Store<BooksState> = inject(Store<BooksState>);
    
      return bookStore.select(BOOKS_SELECTORS.areBooksLoaded).
        pipe(
          filter((value: boolean): boolean => value),
        );
    };
    

    This should work, might need some polishing, because I didn't test it, but this is the way I would implement your case.