19

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?

K.Dᴀᴠɪs
  • 9,945
  • 11
  • 33
  • 43
vachee
  • 395
  • 1
  • 4
  • 15

4 Answers4

33

EDIT: as Ethan mentions below, selectors with props were deprecated in v12. This decision was discussed extensively in an RFC. (Comments further down the thread address how to effectively memoise factory functions.)

The currently recommended approach to this is using a factory function:

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))
);

Previously, NgRx supported 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 })
);
Jordan Gray
  • 16,306
  • 3
  • 53
  • 69
  • 3
    Downvotes are a helpful signal that something might be wrong or missing, and I appreciate you taking the time, but a comment would be really helpful—otherwise, I'm left guessing at how I might have missed the mark! For now, I'm not sure, so I'll leave my answer as it stands. – Jordan Gray Nov 04 '19 at 12:48
  • Does the selector "selectEntitiesByID" memoize the results? If instead of returning a flat array of Entities, what if it constructs a new Dictionary with only those Entities requested? Will that be memoized? I guess the question is how does the memoization compare for equality? – Steve Meierhofer Mar 04 '21 at 14:55
  • Thanks for letting me know @EthanSK, updated now! :) – Jordan Gray Sep 27 '21 at 21:07
3

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]
    );
adrisons
  • 3,443
  • 3
  • 32
  • 48
Colo Ghidini
  • 658
  • 9
  • 20
3

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 }));
Jack
  • 10,313
  • 15
  • 75
  • 118
1

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.

adrisons
  • 3,443
  • 3
  • 32
  • 48
markoffden
  • 1,406
  • 1
  • 15
  • 31