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)
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.
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!
Top comments (7)
Many thanks for sharing this simple example with Ngrx concepts and very good explained!!
I have one doubt, where do you create these new selectors that depends on others? In which module you will include them? In a shared or concrete one?
Cause as far as I know, these selectors combine another selector from other "domains":
I'm asking that because I have facing so many issues regarding nx circular dependencies lint error and I'm trying to understand how would you organise your libs/files.
Many thanks in advance!!
Thanks for the feedback. I really appreciate it.
For Nx workspace, I like to have each slice of my state into different libraries, but actions and models are in separate lib.
Your issue is when you need to create two selectors which combine each other, right ? Never really encounter this. Either put both inside the same file or create a shared state lib containing both.
If you selector is very specific to a template, I like to put it as close as to my template.
I hope this answers your question or if you have a more detailed question, or a stackblitz or any other questions, feel free to reach me on twitter.
Thanks for your response! It really helps me! I still have some doubts:
Again, thanks for your work and your feedback!
I try to copy/paste a slice of my project. I have multiple action library as close as possible from where there are dispatch.
I also have lib for each slice of state located where there are loaded into the store. But if you need this state in multiple location, the state must be place higher in your folder structure.
And for selector, if you are using a selector only for one component, I like to create a file called
my-component.selector.ts
where I put my custom selector. Since I use Component Store for almost all my component, I always put my viewmodel there, but before that, I always created a selector file for my component to declare my viewmodel.Hope this will help you. But I get it, it's hard to get it right first. But don't be scare to move your lib and split them and rename them, ...
I've done it way to many time when I started creating this project for my company. But now it's stable and the structure is nice. I think, I will make an article on NgRx structure inside a Nx monorepo.
Wow, awesome! I really appreciate that you have shared that example! Many thanks!!
I got your point, I'm on that process, renaming and splitting libs haha.
The view model selector, do you include it next to the component (feature folder)? Or in the state one?
Right now I'm using Ngrx store, with no Component Store, but sometimes I doubt to use it as well, as there are some components that do not need to store anything in global (like loading indicators, google map search to select a place...). Do you use both global and local store?
view model must be as close as possible to your component.
xxx.component.ts
xxx.component.html
xxx.selector.ts
Yes I'm using both but mostly component store. In my global store, I only load data that are used across the entire application.
All other state, action are inside a component store.
Tips: A CS doesn't need to be related to only one component, and one component doesn't have to have only one CS.
You can have a CS to open a modal that you use in multiple component.
Most of the time, a state/data/object doesn't need to leave inside a global store. If you have action like reset, unselect, select, this mean your data should belong to a CS.
For loading/error management, i have created a small library on NPM (npmjs.com/package/@tomalaforge/ngr...) because most of my components need this.
Many many thanks for all your help and sorry for all the questions!
I will take a close look to your tips and advices and I will put it on practice!