15
votes

Using @ngrx/entity I want to select an entity by a single id or an array of entities by an array of ids from an entity map.

I do not want the select subscriptions inside a component to be triggered when the entity collection gets a new element or an entity item changes, which I did not select at all.

This obviously happens to me when I use the selectEntities selector and then pick the IDs from the result.

So how can I select 1 or n items by id from an entity collection?

4

4 Answers

20
votes

NgRx supports parameterised selectors by passing props as the last argument to a selector function:

export const selectEntity = createSelector(
  selectEntities,
  (entities, props) => entities[props.id]
);

export const selectEntitiesByID = createSelector(
  selectEntities,
  (entities, props) => props.ids.map(id => entities[id])
);

These are invoked exactly as you might expect:

this.store.pipe(
  select(selectEntity, { id: someID })
);

this.store.pipe(
  select(selectEntitiesByID, { ids: arrayOfIDs })
);

If your ID(s) won't change, you can refactor these into factory functions:

export const selectEntity = id => createSelector(
  selectEntities,
  entities => entities[id]
);

export const selectEntitiesByID = ids => createSelector(
  selectEntities,
  entities => ids.map(id => entities[id])
);

Which are called thus:

this.store.pipe(
  select(selectEntity(someID))
);

this.store.pipe(
  select(selectEntitiesByID(arrayOfIDs))
);
2
votes

Besides entities and ids I also add a selectedEntityId on my state doing on a User Example:

import {User} from '../models/user.model';
import {EntityState, createEntityAdapter} from '@ngrx/entity';

export interface UsersState extends EntityState<User> {
  // additional entities state properties
  selectedUserId: number | null;
}

And the selectors would look like this:

export const selectUserEntities = selectEntities;

export const getSelectedUserId = (state: UsersState) => state.selectedUserId;

export const selectCurrentUser = createSelector(
  selectUserEntities,
  getSelectedUserId,
  (userEntities, userId) => userEntities[userId]
);
1
votes

For both scenarios I would handle it with dedicated selector:

// single entity
export const singleEntitySelector = createSelector(

   // you should have set it up already 
   yourEntitiesObjSelector,

   // here I assume you have set up router reducer state or any other 
state slice where you keep single entity id
   yourIdSelector,

   // then you just return single entity as entities will be an object
   (entities, id) => entities[id]
);

// same for array (you will have to store selected ids also on the 
state tree)
export const selectedEntitiesArraySelector = createSelector(

   // you should have set it up already 
   yourEntitiesObjSelector,

   // here I assume you have set up selected ids store slice
   yourSelectedIdsArraySelector,

   // then you just return entities array reducing ids array
   (entities, idsArray) => idsArray.reduce((acc, id) => {
      return entities[id] ? [...acc, entities[id]] : acc;
   }, [])
);

Then you will just use those selectors in your component, reflecting changes in the view with async pipe as usual. They will reflect all the changes: either there was a single entity id change or ids array change. Do not need to subscribe to anything, unless there is some additional logic in your component.

1
votes

Selectors with props are deprecated, for more info see RFC: Deprecate Selectors With Props.

However, a type safe alternative is demonstrated in RFC 2980. Applied to the answer from @jordan-gray:

export const selectEntity = (props: { id: number }) =>
  createSelector(selectEntities, (entities) => {
    return entities[props.id];
  });

Called with

this.store.select(selectEntity({ id: myId }));