1

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?

Bruno Miguel
  • 1,003
  • 3
  • 13
  • 26

2 Answers2

2

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.

mat.hudak
  • 2,803
  • 2
  • 24
  • 39
1

Thank you @mat.hudak for your reply.

Indeed I had not yet installed the @ngrx/router-store, but based on your post it was more a less evident what I needed to do.

I will leave you with the new implementation, and then make some remarks about it.

books.effects.ts

preFetchBooks$: Observable<{}> = this.createPreFetchBooksEffect();

private createPreFetchBooksEffect(): Observable<{}> {
    return createEffect(() =>
        this.actions$
            .pipe(
                ofType(routerNavigationAction),
                filter(({payload}): boolean => payload.routerState.url === '/books'),
                map(BOOKS_ACTIONS.loadBooks),
                take(1),
            )
    );
}

books.resolver.ts

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),
        );
};

As you can see your suggested implementation was pretty close. The differences are: instead of using switchMap I only need to use the map since dispatch does not return an observable. And most important one is take(1), otherwise it makes an infinte loop.

The resolver is much more simple now.

To be honest I dont really like this resolver any longer, because the only purpose it has now is to prevent the component to be loaded before the effect has time to load the store with books.

Because now with the effect in place and the resolver preventing the premature loading of the book-list-component. In the book-list-component itself I can directly query the store and be sure that there will be books available:

book-list-component.ts

ngAfterViewInit(): void {
        fromEvent(this.bookSearchInputElement.nativeElement, 'input')
            .pipe(
                map((event: Event) => (event.target as HTMLInputElement)?.value),
                startWith(''),
                switchMap((value: string) => this.booksStore.select(BOOKS_SELECTORS.selectBooksByTitle(value))),
                tap((books: BookModel[]) => this.setupBooksFormArray(books)),
                this.takeUntilDestroyRef
            )
            .subscribe();
    }

I have tried to use guards instead of resolver, but the problem with guards is that the route does not triggers until the guard is allwed. Meaning preFetchBook effect would never run...

Any way it has been a really nice ngrx discovery/practice.

Thank you again mate, cheers!

Bruno Miguel
  • 1,003
  • 3
  • 13
  • 26