Home > Software engineering >  NgRx ViewModel confusion
NgRx ViewModel confusion

Time:02-03

Sorry if this doesn't make sense, probably I am missing something fundamental, but I have the following dilemma:

  1. I am receiving a list of items from my backend, similar to:

    interface Item {
      id: number;
      userId: number;
      categoryId: number;
    }
    
  2. 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;
    }
    
  3. 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.

  • Related