Using custom selectors for one-to-one or one-to-many data relationships.

by Kate Sky

When implementing state management with Ngrx we often have a predefined API that can not be changed. I ran across a business case: back end API is written in a way where 2 sets of data that are returned are depend on the other. One-to-one or one-to-many relationship. In order to map entities with corresponding ids we have to create a custom selector.

To make a selector that will do a lookup of another selector just add 4 Actions, 2 Effects and 3 selectors:

This is a declaration of the 2 Actions

export class LoadStudents implements Action{
  readonly type = ActionTypes.LoadStudents;
}
export class LoadTeachers implements Action{
  readonly type = ActionTypes.LoadTeachers;
}
export class Students implements Action{
  readonly type = ActionTypes.Students ;
  constructor(public payload: any){}
}
export class Teachers implements Action{
  readonly type = ActionTypes.Teachers;
  constructor(public payload: any){}
}
export type ActionUnion = LoadStudents| LoadTeachers
| Students  | Teachers  ;

This is an implementation of the 2 effects

@Injectable()
export class Effects {

  constructor(private actions$: Actions, private service: myService ) { 
    }

    @Effect()
    loadStudents$ = this.actions$.pipe(
        ofType<appActions.LoadStudents>(appActions.ActionTypes.LoadStudents),
         switchMap(() => this.service.getStudents().pipe(
          map((data) =>         
             new appActions.Students(data)
         ))));
    

    @Effect()
    loadTeachers$ = this.actions$.pipe(
        ofType<appActions.LoadTeachers >(appActions.ActionTypes.LoadTeachers ),
       switchMap(() => this.service.getTeachers().pipe(
          map((data) =>         
             new appActions.Teachers(data)
         ))));
    
}

When declaring selectors you have to remember that order of loaded data is not predictable and we have to account for it not all being loaded yet:

export interface State {
    csaApp: fromFeatures.State
}
export const reducers:ActionReducerMap<State>= {
    csaApp: fromFeatures.filteredAppReducer

}
const getAppState = createFeatureSelector<fromFeatures.State>('csaApp');
export const getStudents = createSelector(getAppState, (state)=>state.students);
export const getTeachers= createSelector(getAppState, (state)=>state.teachers);
export const getStudentsWithTeachers = createSelector(getStudents, getTeachers,(students, teachers) => {
    if(students === null || teachers === null) return null;
    students.map(s=>s.teacher = teachers.find(t=>t.id ===s.teacherId));
    return students;
}) 

Statement management using Ngrx is a huge subject and creating custom selector will be a great addition to any angular project.

See my github repository for a work-in-progress project I am workig on to support elementary school events for my children’s’ school. https://github.com/katesky/csa-school-app

To see the working (wip) site visit: https://katesky.github.io/csa-school-app