Sorry if this doesn't make sense, probably I am missing something fundamental, but I have the following dilemma:
I am receiving a list of items from my backend, similar to:
interface Item { id: number; userId: number; categoryId: number; }
I also get a list of users and categories and keep them in my store:
interface User { id: number; name: string; } interface Category { id: number; name: string; }
I want to derive an ItemVM view model using these three classes, which will store the derived data:
interface ItemVM { id: number; userName: string; categoryName: string; }
My understanding is that I should create a selector like:
// map userId and categoryId to user.name and category.name
export const selectItemViewModel = createSelector(
// get users, categories, and items
selectUsers,
selectCategories,
selectItems,
// map them
(users, categories, items) => {
return items.map(i => <ItemVM>{
id: i.id,
userName: users.find(u => u.id === i.userId).name,
categoryName: categories.find(c => c.id === i.categoryId).name,
});
}
);
But what I don't understand is, since this selector is not an observable, how do I make sure that users, categories and items are already loaded when this is called?
CodePudding user response:
Selectors are pure functions (with some memoization going on) of the single source of truth state, viewable via a browser extension Redux DevTools.
If you care about loaded
add it to the state per entity.
Following uses latest v15.2.1 @ngrx/entity
Example of Item entity state, repeat per entity
actions
export const apiActions = createActionGroup({
source: 'Item/API',
events: {
'Load Items': emptyProps(),
'Load Items Success': props<{ items: Item[] }>(),
'Load Items Failure': props<{ error: string }>()
},
});
reducer
export interface ItemsState extends EntityState<Item> {
loaded: boolean;
}
export const adapter: EntityAdapter<Item> = createEntityAdapter<Item>();
const initialState: ItemsState = adapter.getInitialState({
// set initial required properties
loaded: false
});
const reducer = createReducer(
initialState,
...
on(apiActions.loadItemsSuccess, (state, { items }) =>
adapter.setAll(items, { ...state, loaded: true })
),
...
);
export const itemsFeature = createFeature({
name: 'items',
reducer
});
// register via StoreModule.forFeature(itemsFeature)
selectors
export const {
// selector for each feature state property
selectEntities,
selectLoaded
} = itemsFeature;
Then compose a multi feature selector
import { selectLoaded as selectItemsLoaded } from '<path>/items.selectors.ts';
import { selectLoaded as selectUsersLoaded } from '<path>/users.selectors.ts';
import { selectLoaded as selectItemsLoaded } from '<path>/categories.selectors.ts';
export const selectItemVmLoaded = createSelector(
selectItemsLoaded,
selectUsersLoaded,
selectItemsLoaded,
(l1, l2, l3) => ({ l1 && l2 && l3 })
)
selectItemVmLoaded
can be added into your selector as an easy way to know whether to just return [] and wait for the api calls.
You can expose in service as, eg. for showing a spinner / skeleton conponents
itemVmLoaded$ = this.store.select(selectItemVmLoaded);
CodePudding user response:
It's a selector which use a distinctUntilChange() and it will be initialized at the bootstrap by the initial state of each reducer so initialize with an empty array not with null ;)
so what you do is correct and everything is fine here.