How to create selector to select an item by id from ngrx store

Viewed 22826

I read ngrx store's documentation about selectors and need to figure out how to create a selector in order to select one item by item id. I already can do this as part of store subscription when I get all of the items from selectAllItems selector, but I need to figure out how to select specific item out of a list of items in the store. My main reason is that createSelector offers performance gains that I like to benefit from.

Here is my code:

import { AppState, Item } from '../../shared/models/index';
import { createSelector } from '@ngrx/store';

export const selectItems = (state: AppState) => state.eventsState;

export const selectAllItems = createSelector(selectItems, (allItems: { items: Item[] }) => {
  if (allItems && allItems.items) {
   return allItems.items.filter((item: Item) => (!item.active && !item.closed));
  } else {
    return allItems.items;
  }
});

Then in my controller i filter by id to get needed item:

this.store.select(selectors.selectAllItems)
  .filter(data => data !== null)
  .subscribe(allItems => {
    this.item = allItems.filter(item => {
      return item.id === this.navParams.data.id;
    });
  });

I would like to be able to create a selector for that specific item and use it like so:

this.store.select(selectors.selectItemById(this.navParams.data.id))
  .filter(data => data !== null)
  .subscribe(selectedItem => {
    this.item = selectedItem;
  });

Any help will be appreciated.

3 Answers

Everything becomes much easier when using entities. When you store them as an object, where key is an id of item and value is the item itself. There are two ways for keeping items as an array - store only ids or full objects of Item.

in reducer:

export interface ItemsState {
  loaded: boolean;
  loading: boolean;
  items: { [key: string]: Item };
  itemIds: any[];
}

const initialState: ItemsState = {
  loaded: true,
  loading: false,
  items: {
    0: {
      foo: 'bar'
    },
    1: {
      bar: 'foo'
    }
  },
  itemIds: [0, 1]
}

in selectors:

export const getItems = (state: ItemsState) => state.items;

export const getItemById = (id) => createSelector(
  getItems, 
  (items) => items[id]
);

in container(component):

constructor(
  private store: Store,
  private route: ActivatedRoute
) {}

getItemOnInit(): void {
  // or any other way to retrieve id from route
  const id = this.route.snapshot.params.id;
  this.store.pipe(select(getItemById(id)));
}

Based on the data available in the store:

export const selectUser = (state: AppState) => state.selectedUser;
export const selectAllBooks = (state: AppState) => state.allBooks;

export const selectVisibleBooks = createSelector(
  selectUser,
  selectAllBooks,
  (selectedUser: User, allBooks: Books[]) => {
    if (selectedUser && allBooks) {
      return allBooks.filter((book: Book) => book.userId === selectedUser.id);
    } else {
          return allBooks;
    }
   }
);

Data that isn't available in the store:

To select a piece of state based on data that isn't available in the store you can pass props to the selector function. These props gets passed through every selector and the projector function. To do so we must specify these props when we use the selector inside our component.

For example if we have a counter and we want to multiply its value, we can add the multiply factor as a prop:

The last argument of a selector or a projector is the props argument, for our example it looks as follows:

export const getCount = createSelector(
  getCounterValue,
  (counter, props) => counter * props.multiply
);

Inside the component we can define the props:

ngOnInit() {
  this.counter = this.store.pipe(select(fromRoot.getCount, { multiply: 2 }))
}

Hers is the Documentation.

Related