2

actually I'm using NGRX (Redux for Angular). At the beginning I have started to store my entities as arrays inside my state. After reading some articles I want to store my entities as hashes while I think it is more performant accessing an entity of entities using the id instead of filtering an array.

But I'm actually unsure doing that the right ngrx way.

Feel free to check my (working - but best practice?) sample here: Ngrx Articles Sample

Inside the articles.component you will find an articleReducer and an ArticlesComponent, that will use the store to get all articles and to get one concrete article.

For each unsure part I have comment that with QUESTION x - ...

If you don't want to check the complete sample, here are the Question Snippets:

Question 0:

export const initialState: ArticleState = {
  foo: 'bar',
  articlesById: [], // QUESTION 0: What is a good usecase to hold an array with the id's when Object.keys(hash) will give the same?
  articleEntities: {}
}

Question 1 + 2:

case GET_ALL_ARTICLES: {

// QUESTION 1: is this a good choice? Or is there a better alternative to create the hash?
   const articleHash: { [id: number]: Article} = {};
   action.payload.forEach(article => articleHash[article.id] = article);

  return {
    ...state,

    // QUESTION 2: is this a good choice? Or is there a better alternative?
    articlesById: action.payload.map(article => article.id),
    articleEntities: articleHash
  }
}

Question 3:

// QUESTION 3 - Getting all articleEntities as Array for ngFor : is this a      good choice? Or is there a better alternative?    
  this.articles$ = this.store.pipe(
    select('articles', 'articleEntities'),
    map(entities => Object.keys(entities).map(id => entities[id]))
  );

Question 4:

const givenIdByRouterMock = 2;
// QUESTION 4 - Getting one concrete Article for given id : is this a good choice? Or is there a better alternative?    
this.article$ = this.store.pipe(
  select('articles', 'articleEntities'),
  map(entities => entities[givenIdByRouterMock])
);

If I will update some article (for instance with id 2) using a new action, I want, that the component including the article will be updated.

Are this valid variants or are there better options, solutions? Feel free to fork the stackblitz sample with other variants/code snippets.

Thanks!

Chris81T
  • 67
  • 1
  • 8

2 Answers2

4

Q0: the ids property can be used to indicate ordering. Another point is if you delete an entity, it's sufficient to only remove it from the this array, which is a simpler task in comparing to remove it from your dictionary.

Q1: your solution works, I prefer using a reduce

Q2: that's fine

return {
  products: action.payload.products.reduce((dict, product) => {
    dict[product.sku] = product;
    return dict;
  }, {}),
  productSkus: action.payload.products.map(product => product.sku),
};

Q3: in your selectors you should indeed transform it to an array. If you're using a ids property (see answer1) you should use ids.map(id => entities[id]).

Q4: indeed to select a specific entity use entities[id]

Extra:

  • NgRx has a package @ngrx/entity which handles state in this manner - unsorted_state_adapter.ts - state_selectors.ts

  • Instead of selecting data with this.store.pipe(select('articles', 'articleEntities')), I think you should take a look at NgRx selectors which is cleaner imho.

  • Since Angular 6.1 you can also use ngFor over a dictionary with the keyvalue pipe.

.

<mat-nav-list>
      <a *ngFor="let member of familyMembers | async | keyvalue" 
        mat-list-item [routerLink]="['/groceries', member.key]"> 
          {{ member.value.avatar }} {{member.value.name}} 
      </a>
</mat-nav-list>
timdeschryver
  • 14,415
  • 1
  • 19
  • 32
  • Hi @timdeschryver for your great answer. Thank you for your extra tips. Especially the "Since Angular 6.1 you can also use ngFor over a dictionary with the keyvalue pipe." That is good to know. I think, I have to check the reduce function. It is actually not 100% clear, how it works. One question to your Q3 answer: How would you select via store the "id's" array and the dictionary with the entities? Do you perform 2 selects and merge them or do you select the whole state? – Chris81T Sep 16 '18 at 11:04
  • Okay, I think I can answer my question myself: After reading the hint to the NgRx Selectors I also have checked the sample app of [NgRx](https://stackblitz.com/run?file=example-app%2Fapp%2Fauth%2Freducers%2Findex.ts) and so I have found samples of combination using the NgRx selectors. – Chris81T Sep 16 '18 at 11:51
  • 1
    Awesome :) if it's not clear feel free to validate your thoughts via gitter. – timdeschryver Sep 16 '18 at 14:02
1

If order matter - use Array as properties order in objects is not guaranteed. To optimize look up in Array we can create selector that transform Array to Object. It's better to keep state normalized (no need to keep the value that we can compute from others).

export interface ArticleState {
  foo: string;
  articles: Article[];
}

export const selectFeature = (state) => state.articleState;
export const selectFeatureArticles = createSelector(selectFeature, state => state.articles);
export const selectFeatureArticlesMap = createSelector(
  selectFeatureArticles,
  articles => _.keyBy(articles, 'id' )
);

QUESTION 0: What is a good usecase to hold an array with the id's when Object.keys(hash) will give the same?

use Array and selector that transform Array to Object

QUESTION 1: is this a good choice? Or is there a better alternative to create the hash?

action.payload.reduce((hash, article) =>({...hash, [article.id]: article}), {});

or Lodash keyBy

_.keyBy(action.payload, 'id' );

QUESTION 2: is this a good choice? Or is there a better alternative?

It's ok.

QUESTION 3 - Getting all articleEntities as Array for ngFor : is this a good choice? Or is there a better alternative?

Store articles in Array.

QUESTION 4 - Getting one concrete Article for given id : is this a good choice? Or is there a better alternative?

I't ok.

Buggy
  • 3,539
  • 1
  • 21
  • 38
  • Hi @Buggy, can you please explain, what "acc" in your statement is? --> "action.payload.reduce((hash, article) =>({...hash, [article.id]: article}), acc);" Actually the reduce method is not 100% clear for me. – Chris81T Sep 16 '18 at 11:09
  • 1
    Thanks, i mean initial value of accumulator, it was typing, edited. – Buggy Sep 16 '18 at 11:42