18

Recently NGRX/Entities have been introduced:

https://medium.com/ngrx/introducing-ngrx-entity-598176456e15 https://github.com/ngrx/platform/tree/master/example-app

And since they are made in a way that an adapter handles a (read: one, single) map-datastructure and on initialization then gets the rest of the reducer state, I was wondering...

Is it possible to hold multiple Entities in one reducer/adapter? The interface says no but maybe there is a hack or it is planned for the future? What if I already have multiple maps in one reducer? Am I forced to split it up or avoid the Entities feature?

The answer below is valid. An alternative approach for (in this example lazily loaded, but not necessarily) modules is to combine reducers with ActionReducerMap:

in your lazy.module.ts:

export interface LazyState {
  lazyAState: LazyAState;
  lazyBState: LazyBState;
}

export const lazyReducers: ActionReducerMap<LazyState> = {
  lazyA: lazyAReducer,
  lazyB: lazyBReducer
};

export interface AppState extends forRoot.AppState {
  lazy: LazyState;
}

@NgModule({
  imports: [
    LazyRoutingModule,
    StoreModule.forFeature('lazy', lazyReducers),
    EffectsModule.forFeature([LazyEffects])
  ]
})
export class LazyModule {
  static forRoot() {
    return {
      ngModule: LazyModule,
      providers: [],
    };
  }
}

And in lazy.selectors.ts (you import the adapters from the reducer-file):

export const getLazyState = createFeatureSelector<LazyState>('lazy');

const getLazyAState = createSelector(getLazyState, (state: LazyState) => state.lazyAState);
const getLazyBState = createSelector(getLazyState, (state: LazyState) => state.lazyBState);

const {selectEntities: lazyASelectEntities, selectAll: lazyASelectAll} = LAZY_A_ADAPTER.getSelectors();

export const lazyADictionary = createSelector(getLazyAState, lazyASelectEntities);
export const lazyAArray = createSelector(getLazyAState, lazyASelectAll);
export const lazyASomeOtherAttributeNotFromAdapter = createSelector(getLazyAState, (state: LazyAState) => state.ids as string[]);

const {selectEntities: lazyBSelectEntities, selectAll: lazyBSelectAll} = LAZY_B_ADAPTER.getSelectors();
// same as for lazy-A, you can also combine selectors if you want
Phil
  • 7,065
  • 8
  • 49
  • 91

1 Answers1

43

NgRx entity is a simple and small library to handle large arrays even thought the documentation does not explain how to use more than one entity in one state, it should be easy since the library what it does behind the scenes is just normalize the array and create a dictionary with the data.

In order to make work the state with one or more entities follow the next steps:

Start by defining a state of each entity.

interface CarState extends EntityState<Car> {
  total: number;
}

interface PlaceState extends EntityState<Place> {
  total: number;
}

Then create a state that holds the entities

export interface State {
  msg: string;
  cars: CarState;
  places: PlaceState;
}

Create the adapters for each entity state to manipulate the data and create the initial states.

const adapterCar = createEntityAdapter<Car>();
const adapterPlace = createEntityAdapter<Place>();

const carInitialState: CarState = adapterCar.getInitialState({ total: 0 });
const placeInitialState: PlaceState = adapterPlace.getInitialState({ total: 0 });

Define the initial global state

const initialState = {
  msg: 'Multiple entities in the same state',
  cars: carInitialState,
  places: placeInitialState
}

Create the reducer:

export function reducer(state: State = initialState, action: ExampleActions): State {

  switch (action.type) {

    case ExampleActionTypes.GetCarList:
      return { ...state, cars: adapterCar.addMany(action.payload, state.cars) };

    case ExampleActionTypes.GetPlaceList:
      return { ...state, places: adapterPlace.addMany(action.payload, state.places) };

    default:
      return state;
  }

}

Expose the selectors

export const selectCarState = (state: State) => state.cars;
export const selectPlaceState = (state: State) => state.places;

export const { selectAll: selectAllCars } = adapterCar.getSelectors();
export const { selectAll: selectAllPlaces } = adapterPlace.getSelectors();

That's it :)

Live example: https://stackblitz.com/edit/angular-multiple-entities-in-same-state

marianocodes
  • 794
  • 1
  • 10
  • 19
  • 1
    yeah, that's what we did back then too. turned out, splitting the reducer/state was a good idea for the architecture as well, because when you get to the point that you need managing multiple maps (instead of some boolean flags), you should probably use a dedicated sub-reducer to keep things clear. – Phil Aug 06 '18 at 07:45
  • @Phil how did you split the reducer? passing a function in the default case? – marianocodes Aug 07 '18 at 04:30
  • I have two reducers/states in two separate files. I will edit my question to show how I combine them – Phil Aug 07 '18 at 04:51
  • just one addition instead of `export const { selectAll: selectAllCars } = adapterCar.getSelectors();`` – alwaysprep Nov 29 '18 at 11:35
  • 2
    instead of this: `export const { selectAll: selectAllCars } = adapterCar.getSelectors();` this worked for me: `export const { selectAll: selectAllCars } = adapterCar.getSelectors(selectCarState);` – alwaysprep Nov 29 '18 at 11:38
  • 3
    Thank you so much for this example, I've been trying to wrap my mind around how to do this for a few weeks, and the ngrx documentation was far to 'simple' for real world usage – Edward Aug 08 '19 at 21:05