英文:
How to proper implement an Angular Resolver using NGRX
问题
我最近几天一直在尝试使用NGRX。
我正在跟随一个Udemy课程,在课程的某个阶段,老师创建了一个解析器来预取一些数据到ngrx存储中,只有在第一次进入给定组件时才会这样做。
想象一下这样的情况:将所有书籍加载到ngrx存储中,但只有在第一次进入书籍列表组件时,并且只有在存储中确实加载了一些书籍时才显示该组件。
在课程中的实现我认为相当糟糕,使用了本地变量、rxjs tap来执行一些副作用,而且最重要的是解析器没有等待数据在存储中可用,它只是立即加载了组件。
所以我想向你展示我的解决方案。我对Rxjs不是很熟悉,这也是我在这种情况下产生疑问的原因。
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),
);
}),
);
};
你认为,这个实现是否适合这个目的?
除了使用解析器服务之外,是否有处理预取全局数据到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?
答案1
得分: 2
我不明白为什么,但有一个NgRx的一部分经常被忽视 - RouterStore。
你可以使用它来监听路由事件并利用其精巧的选择器,这些选择器可以轻松为你提供通常需要从Router、ActivatedRouteSnapshot或其他途径获取的信息。
在你的情况下,我会使用Effect监听routerNavigationAction以及你的解析器的简化版本。
// Effect
prefetchBooks$ = createEffect(() => {
return this.action$.pipe(
ofType(routerNavigationAction),
// 监听每个导航事件 => 仅筛选出你需要的路由
filter(({ payload }) => payload.routerState.url === 'myRoute'),
// 分发预加载数据的操作
switchMap(() => bookStore.dispatch(BOOKS_ACTIONS.loadBooks()))
);
});
// Resolver
// 在此时已经分发了操作,所以只需监听数据
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),
);
};
这应该可以工作,可能需要一些优化,因为我没有测试过,但这是我会实现你的情况的方式。
英文:
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.
答案2
得分: 1
谢谢 @mat.hudak 的回复。
事实上,我还没有安装 @ngrx/router-store,但根据您的帖子,我更清楚需要做什么。
我会留下新的实现,然后对其进行一些评论。
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),
);
};
如您所见,您建议的实现非常接近。区别在于:不需要使用 switchMap,只需使用 map,因为 dispatch 不会返回可观察对象。最重要的是 take(1),否则会产生无限循环。
现在解析器简单得多。
老实说,我不再喜欢这个解析器,因为它现在唯一的目的是在效果有时间加载书店之前阻止组件加载。
因为现在已经有了效果,并且解析器阻止了提前加载书籍列表组件,所以在书籍列表组件中,我可以直接查询存储并确保会有可用的书籍:
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();
}
我尝试使用守卫而不是解析器,但守卫的问题是路由在守卫被允许之前不会触发。这意味着 preFetchBook 效果永远不会运行。
不管怎样,这真的是一个很好的 ngrx 探索/实践。
再次感谢,朋友,干杯!
英文:
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!
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论