I created the following code, trying to ensure the api is not called again when the state is set, and avoid using subscriptions in the component.
loadPackingList$ = createEffect(() => {
return this.actions$.pipe(
ofType(PackingPageActions.loadPackingList),
switchMap((action) =>
of(action).pipe(
withLatestFrom(this.store.select(selectPackingList)),
filter(([action, list]) => !list || list.length === 0),
switchMap(([action, latest]) =>
this.packingService.getPackingList(action.request).pipe(
map((list) => PackingApiActions.loadPackingListSuccess({ list })),
catchError((errors) =>
of(PackingApiActions.loadPackingListFail({ errors }))
)
)
)
)
)
);
});
This more or less works (the api is not called again) and is based amongst others on: How to send request to the server only once by using @NgRx/effects
However, the loading property is still set to true again, which should not happen:
export const packingReducer = createReducer(
initialState,
on(PackingPageActions.loadPackingList, (state) => ({
...state,
loading: true,
})),
component's OnInit:
this.store.dispatch(
PackingPageActions.loadPackingList({ request: this.PACKING_REQUEST })
);
Does anybody have an idea how to fix this?
Thanks!
first you can clean up the stream a bit:
loadPackingList$ = createEffect(() => {
return this.actions$.pipe(
ofType(PackingPageActions.loadPackingList),
withLatestFrom(this.store.select(selectPackingList)),
filter(([action, list]) => !list || list.length === 0),
switchMap(([action, latest]) =>
this.packingService.getPackingList(action.request).pipe(
map((list) => PackingApiActions.loadPackingListSuccess({ list })),
catchError((errors) =>
of(PackingApiActions.loadPackingListFail({ errors }))
)
)
)
);
});
don't need the external switchMap
into of
, just redundant.
second, you'll need to also put a similar check into your reducer to make sure it doesn't get set unless the effect will actually go:
export const packingReducer = createReducer(
initialState,
on(PackingPageActions.loadPackingList, (state) => ({
...state,
loading: !state.whatever.the.path.to.the.list.is?.length,
})),
or instead of filtering, you could just mock responses with the already fetched list:
loadPackingList$ = createEffect(() => {
return this.actions$.pipe(
ofType(PackingPageActions.loadPackingList),
withLatestFrom(this.store.select(selectPackingList)),
switchMap(([action, latest]) =>
((latest && latest.length) ? of(latest) : this.packingService.getPackingList(action.request)).pipe(
map((list) => PackingApiActions.loadPackingListSuccess({ list })),
catchError((errors) =>
of(PackingApiActions.loadPackingListFail({ errors }))
)
)
)
);
});
you could do this in other ways, like having another action in there that signals the request is actually going out like loadPackingListRequest
request that the effect will emit right before the request to signal it's actually happening, and this is what sets the loading
state to true, probably cleaner this way since you don't have a hidden dependency on synchronizing code between your reducer and effects. but it's preference at this point.
EDIT:
the third action strategy would look a bit like this:
loadPackingList$ = createEffect(() => {
return this.actions$.pipe(
ofType(PackingPageActions.loadPackingList),
withLatestFrom(this.store.select(selectPackingList)),
filter(([action, list]) => !list || list.length === 0),
switchMap(([action, latest]) => from([
of(PackingApiActions.loadingPackingList()),
this.packingService.getPackingList(action.request).pipe(
map((list) => PackingApiActions.loadPackingListSuccess({ list })),
catchError((errors) =>
of(PackingApiActions.loadPackingListFail({ errors }))
)
)
]).pipe(mergeAll()))
);
});
this way your effect will emit two actions, one indicating it is actually doing the loading, and another indicating if it failed or succeeded.
then you modify the reducer to look at the loading action instead to set loading to true:
export const packingReducer = createReducer(
initialState,
on(PackingApiActions.loadingPackingList, (state) => ({
...state,
loading: true,
})),