NgRx Effect vs Reducer vs Selector

Thomas Laforge
10 min readNov 18, 2022

--

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 Board
Activity Board

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;
}

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');

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());
}

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());
}

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) {}
}
@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
) {}
}

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'
);

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: [],
}))
);

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,
// ...
}

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
);

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

// in our AppComponent
activities$ = this.store.select(selectActivities);
 <!-- 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>

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) {}
}

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,
};
})
);

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) ?? []
);

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>
// inside AppComponent
getAllTeachersForActivityType$ = (type: ActivityType) =>
this.store.select(selectAllTeachersByActivityType(type));

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;
}
}, []);
}
);

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);

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>

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.

If you found this article useful, please consider supporting my work by giving it some claps👏👏 to help it reach a wider audience. Don’t forget to share it with your teammates who might also find it useful. Your support would be greatly appreciated.

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! Don’t hesitate to ping me if you have more questions

--

--

Thomas Laforge

Software Engineer | GoogleDevExpert in Angular 🅰️ | #AngularChallenges creator