NgRx Effect vs Reducer vs Selector

achtlos

thomas

Posted on November 18, 2022

NgRx Effect vs Reducer vs Selector

Welcome to Angular challenges #2.

The aim of this series of Angular challenges is to increase your skills by practicing on real life exemples. Moreover you will have to submit your work though a PR which I or other can review; as you will do on real work project or if you want to contribute to Open Source Software.

The idea of the second challenge comes from a real life exemple. In NgRx Store, you will find the following concepts: Effects, Reducers and Selectors. And I often see that developers misused them, and more importantly Selectors (which is a key concept) are often misunderstood and underused.

In this challenge, you have a working application using NgRx global store to store our data. But you will have to refactor it to transform the necessary data in the template at the right place using the right NgRx concept

If you haven't done the challenge yet, I invite you to try it first by going to Angular Challenges and then coming back to compare your solution with mine. (You can also submit a PR that I'll review)


For this challenge, you have to display the full list of activities containing the following information (name, main teacher, all teachers practicing the same activity if user is admin)

activity dashboard

To do this, we will start by fetching the backend to retrieve our user and all activities that has the following shape:

export const activityType = [  'Sport',  'Sciences',  'History',  'Maths',  'Physics',] as const;
export type ActivityType = typeof activityType[number];

export interface Person {
  id: number;
  name: string;
}

export interface Activity {
  id: number;
  name: string;
  type: ActivityType;
  teacher: Person;
}
Enter fullscreen mode Exit fullscreen mode

For side effect, NgRx use a concept called Effect. This let us isolate our backend request from the rest of the application. To trigger our Effect, we need to dispatch an Action. An Action is a unique event. 

NgRx Hygiene: UNIQUE is a very important word: You should not reuse action even if you want to trigger the same Effect or Reducer

Let's create two actions: One for fetching the user information, and one for fetching the list of activities.

export const loadActivities = createAction('[AppComponent] Load Activities');
export const loadUsers = createAction('[User] Load User');
Enter fullscreen mode Exit fullscreen mode

The shape of an action should follow some rules : within square brackets shows where the action is being dispatched followed by a brief description of what the action is doing. This can be very helpful when you need to debug your application using the Redux DevTool.

We can now dispatch our action inside our component hook ngOnInit.

ngOnInit(): void {
  this.store.dispatch(loadActivities());
  this.store.dispatch(loadUsers());
}
Enter fullscreen mode Exit fullscreen mode

NgRx Hygiene: you should not have multiple dispatch. A single Action can trigger multiple Effects or multiple Reducers. 

So we can already modify this piece of code:

// single action to dispatch multiple effect to fetch all necessary data
export const initApp = createAction('[AppComponent] initialize Application');

// ngOnInit hook inside our AppComponent
ngOnInit(): void {
    this.store.dispatch(initApp());
  }
Enter fullscreen mode Exit fullscreen mode

Now we can write our effect to trigger our data fetching:

@Injectable()
export class UserEffects {
  loadUsers$ = createEffect(() => {
    return this.actions$.pipe(
      // we listen to only initApp action
      ofType(AppActions.initApp),
      concatMap(() =>
        this.userService.fetchUser().pipe(
          map((user) => UserActions.loadUsersSuccess({ user })),
          catchError((error) => of(UserActions.loadUsersFailure({ error })))
        )
      )
    );
  });

  constructor(private actions$: Actions, private userService: UserService) {}
}
Enter fullscreen mode Exit fullscreen mode
@Injectable()
export class ActivityEffects {
  loadActivities$ = createEffect(() => {
    return this.actions$.pipe(
      ofType(AppActions.initApp), // listen to the same event as UserEffect
      concatMap(() =>
        this.ActivityService.fetchActivities().pipe(
          map((activities) =>
            ActivityActions.loadActivitiesSuccess({ activities })
          ),
          catchError(() =>
            of(ActivityActions.loadActivitiesFailure())
          )
        )
      )
    );
  });

  constructor(
    private actions$: Actions,
    private ActivityService: ActivityService
  ) {}
}
Enter fullscreen mode Exit fullscreen mode

If the http request complete successfully, a new success action is triggered which will update the store. To update the store, we need to use a Reducer

Reducers are a set of pure functions which let use transform the current state of our store to a new state with the action payload

Each Effect must return an action. In our case, we will return either a success action or a failure action. (Don't forget to handle error scenarios!!)

export const loadActivitiesSuccess = createAction(
  '[Activity Effect] Load Activities Success',
  props<{ activities: Activity[] }>() // payload of our success action
);

export const loadActivitiesFailure = createAction(
  '[Activity Effect] Load Activities Failure'
);
Enter fullscreen mode Exit fullscreen mode

And our Reducer looks like this :

// key of activityState inside Store object
export const activityFeatureKey = 'activity';

export interface ActivityState {
  activities: Activity[];
}

// createReducer is a big switch case
export const activityReducer = createReducer(
  initialState,
  // case 1: success
  on(ActivityActions.loadActivitiesSuccess, (state, { activities }) => ({
    ...state,
    activities,
  })),
  // case 2: failure
  on(ActivityActions.loadActivitiesFailure, (state) => ({
    state,
    activities: [],
  }))
);
Enter fullscreen mode Exit fullscreen mode

Remark: "on" function can listen to multiple actions. 

This reducer update only Activity state inside NgRx Global Store. We can divided our store into multiple slices. In our case, we have ActivityState and UserState. (which has a similar reducer).

The Store is just a big javascript object, and each reducer point to a key of that object.

const store = {
     activity: ActivityState,
     user: UserState,
     // ...
}
Enter fullscreen mode Exit fullscreen mode

Now, we need to get this data to our component. This part is easily done thanks to Selectors

Selectors are pure functions to retrieve piece of our state. We can see them as SQL queries. We will query our store to retrieve what's useful for our template to display necessary information.

// select the state under activity key 
export const selectActivityState =
  createFeatureSelector<ActivityState>(activityFeatureKey);

// select the property "activities" defined in Activity State. 
export const selectActivities = createSelector(
  selectActivityState,
  (state) => state.activities
);
Enter fullscreen mode Exit fullscreen mode

We can now easily retrieve the useful piece of our Store by selecting the second Selector.

// in our AppComponent
activities$ = this.store.select(selectActivities);
Enter fullscreen mode Exit fullscreen mode
<!-- template of our AppComponent -->
<h1>Activity Board</h1>
<section>
  <div class="card" *ngFor="let activity of activities$ | async">
    <h2>Activity Name: {{ activity.name }}</h2>
    <p>Main teacher: {{ activity.teacher.name }}</p>
  </div>
</section>
Enter fullscreen mode Exit fullscreen mode

NgRx is strongly coupled with RxJs Observable. This allows us to manage all the asynchronous part of our application.
In our case, the view will be updated when the http request is completed and the store will be updated. If later new activities are added to the store, or just updated, the view will magically refresh.


Everything is very nice, but this was pretty straight forward. Now let's discuss how to build our list of available teachers; called Status.

To do this, we need to get our list of activities and our user. This exercice being inspired by a real life exemple, the author chose to use the concept of Effect for this task. Here are a few reasons (bad or good, we will see later):

  • It's a side effect. The author wanted the data to be processed asynchronously when User and Activities information were available in the store or updated.
  • The author wanted to store the result.
// Status contains all teachers doing the same activity
export interface Status {
  name: ActivityType;
  teachers: Person[];
}

@Injectable()
export class StatusEffects {
  loadStatuses$ = createEffect(() => {
    return this.actions$.pipe(
      ofType(AppActions.initApp), // we can listen to the action dispatched at startup
      concatMap(() =>
        // we cannot use WithLatestFrom to retreive our state since
        // we need to listen to user and activities changes to update our status
        combineLatest([ 
          this.store.select(selectUser),
          this.store.select(selectActivities),
        ]).pipe(
          map(([user, activities]): Status[] => {
            if (user?.isAdmin) {
              // loop over activities to group all teachers by type of activity 
              return activities.reduce(
                (status: Status[], activity): Status[] => {
                  const index = status.findIndex(
                    (s) => s.name === activity.type
                  );
                  if (index === -1) {
                    return [
                      ...status,
                      { name: activity.type, teachers: [activity.teacher] },
                    ];
                  } else {
                    status[index].teachers.push(activity.teacher);
                    return status;
                  }
                },
                []
              );
            }
            return [];
          }),
          // when is done, we return a new action to update our store
          map((statuses) => StatusActions.loadStatusesSuccess({ statuses }))
        )
      )
    );
  });

  constructor(private actions$: Actions, private store: Store) {}
}
Enter fullscreen mode Exit fullscreen mode

And we listen to the success action inside our reducer to update Status state:

export interface StatusState {
 // list of status calculated inside the effect
  statuses: Status[];
 // map the type of one activity type to a given list of teachers
  teachersMap: Map<ActivityType, Person[]>;
}

export const statusReducer = createReducer(
  initialState,
  on(StatusActions.loadStatusesSuccess, (state, { statuses }): StatusState => {
    const map = new Map();
    statuses.forEach((s) => map.set(s.name, s.teachers));
    return {
      ...state,
      statuses,
      teachersMap: map,
    };
  })
);
Enter fullscreen mode Exit fullscreen mode

Inside the Reducer, we can see that the author created a second property teacherMap to easily retrieve the teacher list inside the Selector as you can see below:

export const selectStatusState =
  createFeatureSelector<StatusState>(statusFeatureKey);

export const selectStatuses = createSelector(
  selectStatusState,
  (state) => state.statuses
);

export const selectAllTeachersByActivityType = (name: ActivityType) =>
  createSelector(
    selectStatusState,
    (state) => state.teachersMap.get(name) ?? []
  );
Enter fullscreen mode Exit fullscreen mode

And finally our component looks like this:

<h1>Activity Board</h1>
<section>
  <!-- loop over activity list-->
  <div class="card" *ngFor="let activity of activities$ | async">
    <h2>Activity Name: {{ activity.name }}</h2>
    <p>Main teacher: {{ activity.teacher.name }}</p>
    <span>All teachers available for : {{ activity.type }} are</span>
    <ul>
<!-- for each type of activity, we get the list of teachers from our selector-->
      <li
        *ngFor="
          let teacher of getAllTeachersForActivityType$(activity.type)
            | async
        "
      >
        {{ teacher.name }}
      </li>
    </ul>
  </div>
</section>
Enter fullscreen mode Exit fullscreen mode
// inside AppComponent
getAllTeachersForActivityType$ = (type: ActivityType) =>
  this.store.select(selectAllTeachersByActivityType(type));
Enter fullscreen mode Exit fullscreen mode

For each activity, we call a function to get the list of available teachers from our Store.

The exemple works but have a lot of issues !!!

Issues:

  • We shouldn't store derived state. This is error prone because when your data change, we need to remember all places to update it. We should only have one place of truth with that data, and every transformation should be done inside a Selector.
  • Inside a component, we shouldn't transform the result of a selector (using map operator), or we shouldn't have to call a selector from a function in our view. The data useful for our view should be derived inside a Selector as well.
  • Calling functions inside a template is not good for performance. Each time Angular trigger a Change Detection cycle, the whole template is re-rendered and all functions are recalculated. We will often read to set our components to OnPush. This is a bit better but functions will still be re-executed if activities$ steam is triggered. 
  • Having a combineLatest operator inside an Effect should be a red Flag.

A lot of people starting with NgRx think that every object needed for the template needs to be stored. Don't think like that; one piece of information should be saved only once.

Let me explain the power of Selectors.

One very important piece of information often overlooked is that selector can be combined. We can listen to as many selectors as we want inside another selector and then combined them to get the desired output.

For RxJs developers, selectors is only an enhanced combineLatest operator with memoization.

So the example above can simply be turned into a Selector. No need for Effects or Reducers. Just a nice Selector.

// we combine two selectors
const selectStatuses = createSelector(
  // be as precise as we can be. Don't listen to the whole user object but 
  // only to the necessary properties. This way, the selector will be ONLY
  // rerun if the user's admin property has changed. 
  UserSelectors.isUserAdmin,
  ActivitySelectors.selectActivities,
  (isAdmin, activities) => {
    if (!isAdmin) return [];

    // same code as in previous effect
    return activities.reduce((status: Status[], activity): Status[] => {
      const index = status.findIndex((s) => s.name === activity.type);
      if (index === -1) {
        return [
          ...status,
          { name: activity.type, teachers: [activity.teacher] },
        ];
      } else {
        status[index].teachers.push(activity.teacher);
        return status;
      }
    }, []);
  }
);
Enter fullscreen mode Exit fullscreen mode

This feel more natural. And we deleted StatusEffect and StatusReducer. No need to store Status and teacherMap. And the beauty with memoization is that whenever we call this selector, no calculation is needed, the cached value will be returned. The calculation will only be rerun if the property admin of the user change or the activities have been modified.

And for our template, let's create our activity object.

selectActivities = createSelector(
    ActivitySelectors.selectActivities,
    StatusSelectors.selectStatuses,
    (activities, statuses) =>
      activities.map(({ name, teacher, type }) => ({
        name,
        mainTeacher: teacher,
        type,
        availableTeachers:
          statuses.find((s) => s.name === type)?.teachers ?? [],
      }))
  );

 activities$ = this.store.select(this.selectActivities);
Enter fullscreen mode Exit fullscreen mode

And now the template can simply be written like below. No more function. All necessary properties are available inside our activities$ stream.

<h1>Activity Board</h1>
<section>
  <div class="card" *ngFor="let activity of activities$ | async">
    <h2>Activity Name: {{ activity.name }}</h2>
    <p>Main teacher: {{ activity.mainTeacher.name }}</p>
    <span>All teachers available for : {{ activity.type }} are</span>
    <ul>
      <li *ngFor="let teacher of activity.availableTeachers">
        {{ teacher.name }}
      </li>
    </ul>
  </div>
</section>
Enter fullscreen mode Exit fullscreen mode

Remark: To improve performance a bit, we could have added a trackBy function in our ngFor directive.
 
And here we are, we rewrote our application in a simpler, more readable and more maintainable version by applying the right NgRx concept.

Conclusion:

 
In this simple challenge, we talked about all the key NgRx concepts: Effect, Reducer, Selector and Action.

We have seen that Selectors are often forgotten and misunderstood, and most of the time people chose to store everything. 

If you had one thing to remember, it's to store each piece of information only ONCE. If you need to derive some piece of your store, Selectors should come to mind.


I hope you enjoyed this NgRx challenge and learned from it.

Other challenges are waiting for you at Angular Challenges. Come and try them. I'll be happy to review you!

Follow me on Medium, Twitter or Github to read more about upcoming Challenges!

💖 💪 🙅 🚩
achtlos
thomas

Posted on November 18, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related