Search code examples
javascriptangularrxjsngrxngrx-store

Angular - Ngrx - Updates from multiple sources and component rendering


I've got the following issue and I do need some advice how to tackle this best.

On our application we do a have a container component as following:

ArchiveAllComponent

export class ArchiveAllComponent implements OnInit {

  archiveAllViewModel$: Observable<ArchiveAllViewModel>

  constructor(
    private store: Store<ApplicationState>,
    private votingService: VotingService
  ) { }

  ngOnInit() {
    // Subscribe to any changes in the category and voting entities
    this.archiveAllViewModel$ = this.store.pipe(
      select(selectCategoriesWithVoting),
      map((categories: Category[]) => mapToArchiveAllViewModel(categories))
    )

    // Load all categories
    this.votingService.getCategories().subscribe((categories: Category[]) => {
      this.store.dispatch(new LoadCategoryAction(categories));
    });

    // Load all votings
    this.votingService.getVotings().subscribe((votings: Voting[]) => {
      this.store.dispatch(new LoadVotingAction(votings));
    });
  }

}

Once this component is rendered, two HTTP GET requests are being executed to different API's.

For each of those request an action is dispatched to the store.

Reducer

// Reducer function
export function entityReducer(currentState: Entities, action: EntityActionsUnion): Entities {
    switch (action.type) {   

        case EntityActionTypes.LOAD_CATEGORIES:
            return merge({}, currentState, {categories : action.categories});

        case EntityActionTypes.LOAD_VOTINGS:
            return merge({}, currentState, {votings : action.votings});

        default:
            return currentState;
    }
}

Selectors

export function selectCategoryEntity(state: ApplicationState) {
    return state.entities.categories;
}

export function selectVotingEntity(state: ApplicationState) {
    return state.entities.votings;
}

export const selectCategoriesWithVoting = createSelector(
    selectCategoryEntity,
    selectVotingEntity,
    (categoryEntities: Category[], votingEntities: Voting[]) => {
        if (categoryEntities && categoryEntities.length > 0 && votingEntities && votingEntities.length > 0) {
            let categories = categoryEntities.slice();
            votingEntities.forEach(voting => {
                if (voting.categoryId) {
                    let category = categories.find(x => x.id == voting.categoryId);
                    if(!category.votings) 
                    {
                        category.votings = [];
                    }
                    category.votings.push(voting);
                }
            });

            return categories;
        }

        return [];
    }
);

The archiveAllViewModel$ observable is then passed to some child components for rendering the HTMl accordingly.

This seemed to work on first glance and even if you do a refresh, the following is executed:

  1. Page Refresh
  2. getCategories() + Action / reducer is triggered
  3. getVotings() + Action / reducer is triggered
  4. <archiveElement> + first child element is correctly rendered
  5. <archiveElement> + second child element is correctly rendered
  6. <archiveElement> + third child element is correctly rendered

The problem starts to appear, as soon as I start to navigate away from the component and come back to the same route via client site routing.

<a routerLink="/someotherpage" routerLinkActive="active" mat-button>Other Page</a>

Returning to the same component:

<a routerLink="/archiveAll" routerLinkActive="active" mat-button>Archive</a>

Now compared to a full page refresh, everything is rendered twice:

  1. Navigating away
  2. Navigating back
  3. <archiveElement>
  4. <archiveElement>
  5. <archiveElement>
  6. getCategories() + Action / reducer is triggered
  7. <archiveElement>
  8. <archiveElement>
  9. <archiveElement>
  10. getVotings() + Action / reducer is triggered
  11. <archiveElement>
  12. <archiveElement>
  13. <archiveElement>

As a result each of the the child components now appears twice on the page.

Troubleshooting

The createSelector is now being executed for each http requests once because the predicate categoryEntities && categoryEntities.length > 0 && votingEntities && votingEntities.length > 0 is now no longer only valid for the second http request.

My question

  • What are some best practises to solve issues like that?
  • Are there any operators who can easily solve that for me?

Solution

  • It had nothing to do with Angular. After some further debugging I realised that the issue was within the selector function.

    I've refactored it and now seems to work!

    export const selectCategoriesWithVoting = createSelector(
        selectCategoryEntity,
        selectVotingEntity,
        (categoryEntities: Category[], votingEntities: Voting[]) => {
            if (categoryEntities && categoryEntities.length > 0 && votingEntities && votingEntities.length > 0) {
                let categories = categoryEntities.slice();       
                votingEntities.forEach(newVoting => {
                    if (newVoting.categoryId) {
                        let category = categories.find(x => x.id == newVoting.categoryId);
                        if(!category.votings) 
                        {
                            category.votings = [];
                        }                 
    
                        let oldVotingIndex = category.votings.findIndex(x => x.id == newVoting.id);
                        if(oldVotingIndex != -1)
                        {
                            category.votings[oldVotingIndex] = newVoting;
                        } else {
                            category.votings.push(newVoting);
                        }
                    }
                });
    
                return categories;
            }
    
            return [];
        }
    );