Skip to content

Commit 46ecb00

Browse files
author
Armen Vardanyan
committed
Chapter 12
1 parent adf3d84 commit 46ecb00

File tree

4 files changed

+242
-1
lines changed

4 files changed

+242
-1
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Effects in depth
2+
3+
## HTTP is not the only type of side effects
4+
5+
In the previous chapter, we have learned about using NgRx `Effects` to perform HTTP requests through the Store-State architecture. In this chapter, we are going to explore some more complicated cases and tie together some things.
6+
7+
Let's explore the following scenario: we want to show toast notifications whenever there is a successful addition/deletion of a category. As we are using Angular Material, we are going to use its own `Snackbar` component, which is being triggered via a special service. First of all, let's go on and add the `SnackbarModule` to our `AppModule`. Then, let's understand how it operates in relation to NgRx `Store` and `Effects`. It is an action tangential to some operations on our `Store` data, meaning it is, in fact, an `Effect`. Now let's discuss how it will be implemented. Naturally, we are going to have many such action success messages, so we want such a solution that does not require us to write a specific effect handler for each HTTP call success message. The best approach would be such that adds an optional string `message` to the HTTP call success `Actions` payload, which would then be displayed by one single `Effect`. Let's rewrite our category add success action:
8+
9+
```ts
10+
export const addCategorySuccess = createAction('[Category List] Add Category Success', props<{payload: {data: Category, message?: string}}>());
11+
```
12+
13+
As you see, we changed the payload from just being a category object to a wrapper object that separately contains the response data and the message. Now, we only have to write a handler that will display the success message in the UI.
14+
15+
This effect handler, though, cannot be in a class called `CategoriesEffects`, because our app will contain multiple calls and many of them will not be related to categories at all. But we will learn how to use multiple effects (and lazy load chunks of states/effects per modules) in future chapters, so for now let's put the handler in our only existing `Effect` class (notice we will also have to rewrite our `Reducer` slightly because of the payload type change, but we will leave this as a small exercise for the reader):
16+
17+
```ts
18+
export class CategoriesEffects {
19+
20+
// other effect handlers omitted for brevity
21+
22+
addCategory$ = createEffect(() => this.actions$.pipe(
23+
ofType(addCategory),
24+
mergeMap(({payload}) => this.categoriesService.addCategory(payload).pipe(
25+
map((result) => addCategorySuccess({payload: {data: result, message: 'Category successfully added'}})),
26+
catchError(() => of(addCategoryError())),
27+
)),
28+
));
29+
30+
handleSuccessMessage$ = createEffect(() => this.actions$.pipe(
31+
ofType(addCategorySuccess),
32+
tap(({payload}) => this.snackBar.open(payload.message, 'Dismiss', {duration: 2000})),
33+
));
34+
35+
constructor(
36+
private readonly actions$: Actions,
37+
private readonly categoriesService: CategoryService,
38+
// we injected the snackbar service to use
39+
private readonly snackBar: MatSnackBar,
40+
) { }
41+
42+
}
43+
```
44+
45+
## Not all effects should dispatch
46+
47+
You might notice that out Effect handler does not `map` the piped `Action` to another `Action` as in all other cases; that is because after performing this `Action` there is nothing else we have to do in terms of our `State`. This is a side effect purely for the purpose of the side effect. But this (not mapping to another `Action`) will actually cause a `TypeError`, as NgRx expects the Effect stream to map to an `Action`. So how do we fix this?
48+
49+
```ts
50+
export class CategoriesEffects {
51+
52+
// other effect handlers omitted for brevity
53+
54+
handleSuccessMessage$ = createEffect(() => this.actions$.pipe(
55+
ofType(addCategorySuccess),
56+
tap(({payload}) => this.snackBar.open(payload.message, 'Dismiss', {duration: 2000})),
57+
) {dispatch: false});
58+
59+
// constructor omitted
60+
61+
}
62+
```
63+
64+
The `dispatch: false` flag is used to indicate to NgRx that this particular effect is not impacting the `Store`, so it won't be dispatching a resulting `Action`. use this flag whenever you are performing effects on actions that do not result in other actions.
65+
66+
## Handling multiple Effects
67+
68+
But what about other success messages? Surely, we are not going to write effect handlers for each and every success message action? Turns out, NgRx got us covered; here is how we handle multiple actions in one effect:
69+
70+
```ts
71+
export class CategoriesEffects {
72+
73+
// other effect handlers omitted for brevity
74+
75+
handleSuccessMessage$ = createEffect(() => this.actions$.pipe(
76+
ofType(addCategorySuccess, deleteCategorySuccess),
77+
tap(({payload}) => this.snackBar.open(payload.message, 'Dismiss', {duration: 2000})),
78+
));
79+
80+
// constructor omitted
81+
82+
}
83+
```
84+
85+
So the `ofType` operator can accept multiple actions and handle if any of them is dispatched. Let's understand how the `payload` type is being inferred by NgRx. If we combine actions A and B with payloads of type X and Y respectively using `ofType`, the resulting type will be `X | Y`, meaning it will contain only properties that are present on both action types. In our case, we modified the `deleteCategorySuccess` action so that its payload also contains a `message` optional property of type `string`. Thus, the resulting payload type is an object `{message?: string}`, which is perfect for our case.
86+
87+
## Homework
88+
89+
Tasks for this homework are going to be pretty simplistic
90+
91+
1. Add handlers for all success messages
92+
2. Create handlers for error messages too.
93+
94+
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# NgRx and Lazy Loading
2+
3+
## Feature states
4+
5+
[Lazy loading](https://angular.io/guide/lazy-loading-ngmodules) is an important Angular feature that boosts performance and allows for better, modular architecture. Basically, we split our app into feature modules, each corresponding to some app-specific feature, and then load those modules when needed (when the user navigates to them). Lazy loading is a built-in feature in Angular.
6+
7+
So how does NgRx relate to this? Let's examine the structure of our app to understand better.
8+
9+
In our financial logger app we have at least two features: categories and logs. We might want to have pages that show all the logs, add new logs, add categories, and so on, so it makes sense to keep them separated.
10+
11+
But we have mentioned that the `State` in our `Store` is basically a large global object with no write access, meaning that we probably would want to define all the feature state slices beforehand; but that would kind of break the lazy loading logic: if the user never visits the category module pages, why would we even have any state related to categories stored?
12+
13+
Thankfully, NgRx got us covered.
14+
15+
## NgRx Feature states
16+
17+
In NgRx, data from the lazy loaded modules can be stored in what is called Feature states; basically, our `Store` now will contain data like this:
18+
19+
```ts
20+
export interface AppState {
21+
categories: CategoryState;
22+
logs: LogState;
23+
}
24+
```
25+
26+
The trick here being that, say, `logs` is undefined from the start of the application until the user visits a page from the `LogsModule`.
27+
28+
## Preparing for feature states
29+
30+
Let us first create our first lazy loaded module: `LogsModule` and include it in our routing. Now, because `LogsModule` is a separate entity, it is going to have a separate `State`. Inside `LogsModule`, create a folder named `state`, with the same files we have in the root directory `state` folder. Then, let's design our feature state:
31+
32+
```ts
33+
// src/app/logs/state/state.ts
34+
export enum LogType {
35+
Spend = 'Spend',
36+
Income = 'Income',
37+
}
38+
39+
export interface Log {
40+
title: string;
41+
date: string;
42+
amount: number;
43+
type: LogType;
44+
categoryId: number;
45+
}
46+
47+
export interface LogsState {
48+
logs: Log[];
49+
loading: {
50+
list: boolean;
51+
add: boolean;
52+
};
53+
}
54+
55+
export const initialState: LogsState = {
56+
logs: [],
57+
loading: {
58+
list: false,
59+
add: false,
60+
},
61+
};
62+
```
63+
64+
So this is how our feature state will look like. It does not reference anything from the `AppState`, which is good, as our module is independent and lazy loaded. So how do we connect this independent state with the global, `AppState`? First, we will have to write a reducer. In the `database.json` file we also have a nested object called `logs`, which is an empty array. For the purpose of providing the opportunity to grow experience, we will skip writing the reducer and allow the reader to do that themselves.
65+
66+
Write a `logsReducer` and `LogsEffects` and come back to this chapter.
67+
68+
## Lazy loading feature states
69+
70+
Now our states are inside a lazy loaded modules. How do we plug them into the existing `Store`? Turns out, this is pretty easy. In the `logs.module.ts`, add these lines:
71+
72+
```ts
73+
// src/app/logs/logs.module.ts
74+
75+
// import statements omitted
76+
77+
@NgModule({
78+
// other metadata
79+
imports: [
80+
// other imports
81+
StoreModule.forFeature('logsFeature', logsReducer),
82+
EffectsModule.forFeature([LogsEffects]),
83+
]
84+
})
85+
export class LogsModule {}
86+
```
87+
88+
As you have noticed, instead of `forRoot` we used methods called `forFeature`, which indicate these are reducers and effects tht are being added dynamically, after the user visits this particular module. The `StoreModule.forFeature` method's first argument is the name of the feature state, which is being used when writing feature specific selectors. Let's now write a selector that gets the list of logs (the empty array from the beginning of the chapter).
89+
90+
```ts
91+
// src/app/logs/state/selectors.ts
92+
93+
export const logs = (state: AppState) => state.logsFeature.logs;
94+
```
95+
96+
The name `logsFeature` comes from the `StoreModule.forFeature` method's first argument. NgRx provides an easy way to make less boilerplate and not have to use that name each time, by using a special `createFeatureSelector` function. Let's rewrite our selector and see it in action:
97+
98+
```ts
99+
// src/app/logs/state/selectors.ts
100+
101+
const logsFeature = createFeatureSelector('logsFeature');
102+
103+
export const logs = createSelector(logsFeature, state => state.logs); // state here is already the logsFeature `State`
104+
```
105+
106+
I personally use a small trick to write even less code:
107+
108+
```ts
109+
// src/app/logs/state/selectors.ts
110+
111+
const logsFeature = createFeatureSelector('logsFeature');
112+
const selector = <T>(mapping: (state: LogsState) => T) => createSelector(logsFeature, mapping);
113+
export const logs = selector(state => state.logs);
114+
```
115+
116+
Now our own `selector` function is a small wrapper (or a type of functions known as "partial application") around `createSelector`, which always provides the first argument, so we don't have to type it every time.
117+
118+
Now we already have a functioning lazy loaded feature state.
119+
120+
## Where to be careful
121+
122+
Notice that our `AppState` is and remains a single, unique object. It is just that before the lazy loaded routes are visited, the corresponding nested feature states are `undefined`. So this may create some problems in the future if we are not careful. Imagine a scenario when we have new module, that now needs the `logs` data. Because `logs` is a lazy loaded feature, that data may or may not be available depending on whether the user had previously visited the `logs` routed pages. So if we need some data somewhere, we have to make sure that data is available higher up, and not in another lazy loaded module. As an exercise, try to determine if we need to make the `categories` a separately loaded feature module like `logs`
123+
124+
## Homework
125+
126+
Homework for this chapter is quite extensive and non-specific: implement the whole lifecycle for financial logs: creating new logs, deleting them, and so on. Future chapters will assume those are implemented, and this will also be an important exercise for our newly acquired skills. If you want any sort of hints and guidance, feel free to take a look at the example app, where all the features are implemented.

src/assets/content/homeworks/homework-10.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
### Homework example 1:
22
```ts
3-
// in the category-list-presenter.ts file
3+
// in the effects.ts file
44
export class CategoriesEffects {
55
deleteCategory$ = createEffect(() => this.actions$.pipe(
66
ofType(deleteCategory),
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
### Homework example 1:
2+
```ts
3+
// in the effects.ts file
4+
export class CategoriesEffects {
5+
handleSuccessMessage$ = createEffect(() => this.actions$.pipe(
6+
// if you already have category update functionality covered
7+
ofType(addCategorySuccess, deleteCategorySuccess, updateCategorySuccess),
8+
tap(({payload}) => this.snackBar.open(payload.message, 'Dismiss', {duration: 2000})),
9+
));
10+
}
11+
```
12+
13+
### Homework example 2:
14+
```ts
15+
export class CategoriesEffects {
16+
handleErrorMessage$ = createEffect(() => this.actions$.pipe(
17+
ofType(addCategoryError, deleteCategoryError),
18+
tap(({payload}) => this.snackBar.open(payload.message, 'Dismiss', {duration: 2000})),
19+
));
20+
}
21+
```

0 commit comments

Comments
 (0)