I have a function which checks whether a grid has loaded or not and if not it triggers the loading. But currently this ends up firing several times for the same Loaded value so it will call the relevant action multiple times. I was under the impression that store selectors emit by default only distinct (changed) values?
My function
private gridLoaded(filters: FilteredListingInput): Observable<boolean> {
return this.settings.states.Loaded.pipe(
tap(loaded => {
this.logService.debug(
`Grid ID=<${
this.settings.id
}> this.settings.states.Loaded state = ${loaded}`
);
// Now we get duplicate firings of this action.
if (!loaded) {
this.logService.debug(
`Grid Id=<${
this.settings.id
}> Dispatching action this.settings.stateActions.Read`
);
this.store.dispatch(
new this.settings.stateActions.Read(filters)
);
}
}),
filter(loaded => loaded),
take(1)
);
}
this.settings.states.Loaded is selector from NgRx store.
The logging output I get looks like this:
Grid ID=<grid-reviewItem> this.settings.states.Loaded state = false {ignoreIntercept: true}
Grid Id=<grid-reviewItem> Dispatching action this.settings.stateActions.Read {ignoreIntercept: true}
Grid ID=<grid-reviewItem> this.settings.states.Loaded state = true {ignoreIntercept: true}
Grid ID=<grid-reviewItem> Calling FilterClientSide action. Loaded=true {ignoreIntercept: true}
Grid ID=<grid-reviewItem> this.settings.states.Loaded state = true {ignoreIntercept: true}
Grid ID=<grid-reviewItem> Calling FilterClientSide action. Loaded=true {ignoreIntercept: true}
Grid ID=<grid-reviewItem> this.settings.states.Loaded state = true {ignoreIntercept: true}
Grid ID=<grid-reviewItem> Calling FilterClientSide action. Loaded=true {ignoreIntercept: true}
How can I make sure that the relevant actions are triggered only once?
Edit - updates
Selector code:
export const getReviewItemsLoaded = createSelector(
getReviewItemState,
fromReviewItems.getReviewItemsLoaded
);
export const getReviewItemState = createSelector(
fromFeature.getForecastState,
(state: fromFeature.ForecastState) => {
return state.reviewItems;
}
);
export const getReviewItemsLoaded = (state: GridNgrxState<ReviewItemListDto>) =>
state.loaded;
export interface GridNgrxState<TItemListDto> {
allItems: TItemListDto[];
filteredItems: TItemListDto[];
totalCount: number;
filters: FilteredListingInput;
loaded: boolean;
loading: boolean;
selectedItems: TItemListDto[];
}
As you can see we are just getting the state.loaded property, it's a trivial selector.
Reducers that change the loading property:
export function loadItemsSuccessReducer(state: any, action: GridAction) {
const data = action.payload;
return {
...state,
loading: false,
loaded: true,
totalCount: data.totalCount ? data.totalCount : data.items.length,
allItems: data.items
};
}
export function loadItemsReducer(state: any, action: GridAction) {
return {
...state,
loading: true,
filters: action.payload
};
}
export function loadItemsFailReducer(state: any, action: GridAction) {
return {
...state,
loading: false,
loaded: false
};
}
Actions
export class LoadReviewItemsAction implements Action {
readonly type = LOAD_REVIEWITEMS;
constructor(public payload?: FilteredListingInput) {}
}
export class LoadReviewItemsFailAction implements Action {
readonly type = LOAD_REVIEWITEMS_FAIL;
constructor(public payload: any) {}
}
export class LoadReviewItemsSuccessAction implements Action {
readonly type = LOAD_REVIEWITEMS_SUCCESS;
constructor(public payload: PagedResultDtoOfReviewItemListDto) {}
Effects
export class ReviewItemsEffects {
constructor(
private actions$: Actions,
private reviewItemApi: ReviewItemApi
) {}
@Effect()
loadReviewItems$ = this.actions$
.ofType(reviewItemActions.LOAD_REVIEWITEMS)
.pipe(
switchMap((action: reviewItemActions.LoadReviewItemsAction) => {
return this.getDataFromApi(action.payload);
})
);
/**
* Retrieves and filters data from API
*/
private getDataFromApi(filters: FilteredListingInput) {
return this.reviewItemApi.getReviewItems(filters || {}).pipe(
map(
reviewItems =>
new reviewItemActions.LoadReviewItemsSuccessAction(
reviewItems
)
),
catchError(error =>
of(new reviewItemActions.LoadReviewItemsFailAction(error))
)
);
}
}
distinctoperator rxjs.dev/api/operators/distinct. Isn't that what you want? - martincreateSelectoris memoized, so it shouldn't execute if the input parameters remain the same. From the code shared it's hard to say what's going wrong - if you have a stackblitz I'm happy to take a look. - timdeschryverdistinctUntilChangedhowever it didn't work. I've tried this without success:return this.settings.states.Loaded.pipe(distinctUntilChanged()).pipe( tap(loaded => { //... handle loading }), filter(loaded => loaded), take(1) );Maybe I did it wrong? - Botond Béres