Skip to content

Commit adf3d84

Browse files
author
Armen Vardanyan
committed
Fixes
1 parent 452a2f9 commit adf3d84

File tree

9 files changed

+352
-10
lines changed

9 files changed

+352
-10
lines changed

src/app/app.component.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export class AppComponent implements OnInit {
2626
ngOnInit() {
2727
this.router.events.pipe(
2828
filter(event => event instanceof NavigationEnd),
29-
).subscribe(() => window.scrollTo({top: 0, behavior: 'smooth'}));
29+
).subscribe(() => {
30+
window.scrollTo({top: 0, behavior: 'smooth'})
31+
});
3032
}
3133
}
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
# Working with side effects
2+
3+
## What are side effects?
4+
5+
When working with NgRx (and probably most other state management systems) you will often encounter the phrase "side effects". But what are those really? Let's examine this concept closely
6+
7+
So far, we have built an application that is pretty straightforward, it takes some data (in our case from the `Store`), and renders it to the UI. We can also modify that data using an `Action`. Again this is a very simple relationship: `Store` -> Data -> `Selectors` -> UI -> Events (like a user adding a financial record) -> `Action` -> `Reducer` -> `Store` and so on. But what about modifying things that are not part of the `Store`, but are inevitably related to it?
8+
9+
You might ask, why do we need NgRx to deal with logic that is not directly a part of it? Let's look at a most popular example.
10+
As mentioned in the previous chapter, in a real web application the data usually comes from a remote server, a persistent database that exposes methods through an API which we can use to retrieve, add, modify and delete that data. Of course, in an app that uses ngRx we would want to store that data in the `Store` and access it through a `Selector`. But how do we put that data inside the `Store` in the first place? We can't just make an HTTP request in the `initialState` of the `Store`; we might need parameters, and also we cannot inject our service from the previous chapter there. Of course, we could do the following:
11+
12+
```ts
13+
// src/app/category-list/category-list-container/category-list-container.component.ts
14+
import { Component, OnInit } from '@angular/core';
15+
import { Store } from '@ngrx/store';
16+
17+
import { categories } from '../../state/selectors';
18+
import { storeCategories } from '../../state/actions';
19+
20+
@Component({
21+
selector: 'app-category-list-presenter',
22+
template: '<app-category-list-presenter [categories]="categories$ | async"></app-category-list-presenter>',
23+
})
24+
export class CategoryListContainer implements OnInit {
25+
categories$ = this.store.select(categories);
26+
27+
constructor (
28+
private readonly store: Store,
29+
private readonly categoryService: CategoryService,
30+
) {}
31+
32+
ngOnInit() {
33+
this.categoryService.getCategories().subscribe(categories => this.store.dispatch(storeCategories({payload: categories})));
34+
}
35+
}
36+
```
37+
38+
In this example, we used our component to get the data from the service, then dispatch a new action that stores that categories in the application `State`, so that we can use the selector to retrieve and display that data in the same component.
39+
40+
I am specifically not writing the action and the reducer part of this code, because we are not going to use this code at all, as this defeats the whole purpose of using NgRx in the first place. Think about it:
41+
42+
1. We want our components to deal with as little business logic as possible, but in this case we have a component that goes through the entire hassle of making an HTTP call and storing the data
43+
2. We want less code in our components, but some real world components might load huge chunks of different data and make dozens of HTTP requests; if we start making all of those HTTP requests, we will end up with a huge, bloated component
44+
3. Ideally, we want a component to say "I am here, please give me my data", and just receive that data through selectors, without knowing where exactly that data came from
45+
46+
So, essentially, loading data from a remote server is a side effect in our case; it is necessary, but it is not a part of the direct NgRx lifecycle we mentioned previously. So how do we deal with this problem?
47+
48+
## @ngrx/effects
49+
50+
Thankfully NgRx offers another tool for solving exactly this issue, called `@ngrx/effects`.
51+
52+
Essentially, `@ngrx/effects` is a library that provides functions that help us create handlers for side effects, like making HTTP calls that impact the state, and so on. Let's begin by bringing it to our application
53+
54+
```bash
55+
npm install @ngrx/effects
56+
```
57+
58+
Now we have the effects library in our application. Let's register it in our `AppModule`:
59+
60+
```ts
61+
// other imports omitted
62+
import { EffectsModule } from '@ngrx/effects';
63+
64+
@NgModule({
65+
// other metadata omitted
66+
imports: [
67+
// other imports omitted
68+
EffectsModule.forRoot([]),
69+
],
70+
})
71+
export class AppModule {}
72+
```
73+
74+
As you see, we imported the `EffectsModule` and registered it with an empty array. This (for now) empty array is where our effects will go.
75+
76+
But what are NgRx effects? An `Effect` is an `Injectable` class (a service if we put it a bit harshly) that registers side-effect handling functions. Let's create an `effects.ts` file in our `state` folder, write an empty `Effect` there and register it:
77+
78+
```ts
79+
@Injectable()
80+
export class CategoriesEffects {
81+
82+
}
83+
```
84+
85+
An in `AppModule`:
86+
```ts
87+
// other imports omitted
88+
import { CategoriesEffects } from './state/effects.ts';
89+
90+
@NgModule({
91+
// other metadata omitted
92+
imports: [
93+
// other imports omitted
94+
EffectsModule.forRoot([CategoriesEffects]),
95+
],
96+
})
97+
export class AppModule {}
98+
```
99+
100+
Now we have a registered `Effect`, and NgRx will invoke its handlers when necessary. But we haven't written any handlers yet! Before we do, let's understand the theory behind how all this works:
101+
102+
1. As with everything in NgRx, the central thing is an `Action`, that gets dispatched an tells NgRx to please perform a specific side effect
103+
2. Then we have a handler, that is an `Observable` stream that converts our `Action` to some concrete function, say an HTTP request
104+
3. Then that stream gets mapped to another `Action` that impacts the store, say, stores the retrieved information
105+
4. Everything is done using RxJS streams, so we are going to up our knowledge of RxJS
106+
107+
In our case, retrieving and storing the categories list is going to have the following steps:
108+
109+
1. An action is dispatched telling NgRx that the categories component has been initialized
110+
2. An Effect handler gets invoked on our`Action`, makes the HTTP call to our API
111+
3. The returned result is being mapped to another `Action`, say `loadCategoriesSuccess`, which puts the data in the `Store` through a reducer, something we already are familiar with
112+
113+
Let's start setting pieces for this
114+
115+
First of all in our `actions.ts` file let's create the corresponding actions:
116+
117+
```ts
118+
// other imports omitted
119+
import { Category } from './models';
120+
121+
// other actions omitted
122+
123+
export const categoriesListLoaded = createAction('[Category List] Categories List Loaded');
124+
export const loadCategoriesSuccess = createAction('[Category List] Load Categories Success', props<{payload: Category[]}>());
125+
export const loadCategoriesError = createAction('[Category List] Load Categories Error');
126+
```
127+
128+
As you can see, we have created the actions we mentioned, and also a specific `Action` that will get invoked when our HTTP call fails (these kinds of things tend to happen from time to time, and we need error handling)
129+
130+
Let's also put the success logic in our reducer function: we need to put the categories list when it is successfully retrieved:
131+
132+
```ts
133+
import { addCategory, loadCategorySuccess } from './actions';
134+
135+
// other imports omitted
136+
137+
const _reducer = createReducer(
138+
initialState,
139+
// other handlers omitted
140+
on(loadCategorySuccess, (state, action) => ({...state, categories: action.payload})),
141+
);
142+
```
143+
144+
Now we will be able to modify our state an put the categories upon successful retrieval. Let's now get down to the most important thing: creating a side effect handler
145+
146+
## How are handlers registered?
147+
148+
All handlers are `Observable` streams as mentioned earlier, to which NgRx will subscribe and perform them when the corresponding `Action` gets dispatched. Here is how it works:
149+
150+
1. NgRx provides a `createEffect` function that is used to register a handler
151+
2. It also provides us with a specific `Observable` called `Actions`. It is a stream of all actions dispatched in the app; basically, whenever any action gets dispatched throughout the application, this `Observable` will emit it
152+
3. It provides us with a custom RxJS operator called `ofType`, which allows us to filter out the specific actions we need for this particular side effect
153+
4. We can then use an operator like `mergeMap` to redirect our `Observable` to a service call that gets the data from the API.
154+
5. We need to use the `map` operator and change the result of our HTTP call to an `Action` that puts that result in the `Store` (i.e. `loadCategoriesSuccess`)
155+
6. We can use `catchError` to map the stream to the error handling `Action` (i.e. `loadCategoriesError`) if the request fails
156+
7. In other places, we can write other effects that handle error actions separately
157+
158+
Let's see it all in action:
159+
160+
```ts
161+
import { Actions, createEffect, ofType } from '@ngrx/effects';
162+
import { of } from 'rxjs';
163+
import { mergeMap, map, catchError } from 'rxjs/operators';
164+
165+
import { CategoryService } from '../services/category.service';
166+
import { categoriesListLoaded, loadCategoriesSuccess, loadCategoriesError } from './actions';
167+
168+
export class CategoriesEffects {
169+
170+
categoriesListLoaded$ = createEffect(() => this.actions$.pipe(
171+
ofType(categoriesListLoaded),
172+
mergeMap(() => this.categoriesService.getCategories().pipe(
173+
map(categories => loadCategoriesSuccess({payload: categories})),
174+
catchError(() => of(loadCategoriesError())),
175+
))
176+
));
177+
178+
constructor(
179+
private readonly actions$: Actions,
180+
private readonly categoriesService: CategoryService,
181+
) { }
182+
183+
}
184+
```
185+
186+
Let's look at this in depth. Here, `categoriesListLoaded$` is an `Observable` handler for the side effect that will get invoked when the `CategoryListContainer` gets initialized and indicated it wants data; because we registered the `Effect` class in the `EffectsModule`, NgRx will subscribe to it for us and wait for action. `createEffect` function takes a callback that returns the handler. The handler itself takes the `Actions` `Observable`, uses the `ofType` operator to specify which exact `Action` we need, then uses `mergeMap` to redirect our stream to the HTTP call that our service makes, and then maps it to the success `Action` when the request is performed successfully, and to an error `Action` when it fails.
187+
188+
The last thing we need to do is dispatch the `Action` that triggers this whole thing from the component:
189+
190+
```ts
191+
export class CategoryListContainerComponent implements OnInit {
192+
categories$ = this.store.select(categories)
193+
194+
constructor(
195+
private readonly store: Store,
196+
) { }
197+
198+
ngOnInit() {
199+
this.store.dispatch(categoriesListLoaded());
200+
}
201+
202+
// other methods omitted
203+
204+
}
205+
```
206+
207+
So here is the lifecycle of this component:
208+
209+
1. When initialized, it dispatches the `categoriesListLoaded` `Action`
210+
2. The `Effect` gets triggered because we used `ofType`
211+
3. An HTTP call is made
212+
4. The result of that call is mapped to the action `loadCategoriesSuccess` with the corresponding payload (the list of categories)
213+
5. Because NgRx is subscribed to the `categoriesListLoaded$` `Observable`, it receives the `Action` and dispatches it to the `Store`, triggering the `Reducer` function
214+
6. In the reducer our specific handler receives the categories payload and puts it in the `Store`
215+
7. NgRx propagates the changes to the components
216+
8. Our component has used a `Selector` to get the categories list data, and it will automatically receive that data when this cycle is complete
217+
9. That's it!
218+
219+
Usually effects require 3 actions: one which triggers the effect handler, another one to propagate the successful result to the reducer to change the `State`, and one that is dispatched where we have an error.
220+
221+
## Homework
222+
223+
We created a flow in which we get the categories data from the remote API. As you remember, in [Chapter 8](/chapters/8) we created a delete button that removes a category, and also implemented a feature that allows the user to add a new category. So after completing this chapter you should try to:
224+
225+
1. Make the delete button actually delete the category fro the database, using our service method and a new effect
226+
2. Have the same for adding a category
227+
228+
*Hint*: the `Actions` `Observable` emits action objects themselves, so in the `mergeMap` callback you can access the action object and its payload using the argument like this: `mergeMap((action) => doSomethingWithPayload(action.payload))`
229+
230+
*Note*: You will have to modify the components and the reducer function also; those are not directly relevant to this chapter's work, so won't be included in the solution example
231+
232+
Now we have learned about effects, one of the most important features of NgRx. In the next chapter, we will dive a bit deeper and see what other use cases (apart from making HTTP requests) NgRx Effects have.

src/assets/content/chapters/chapter-5.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## What are Actions?
44

5-
`Actions` are the most simple core concept of NgRx (and Redux in general). Action is a unique event that is used to trigger a change in the state. What does it mean? For example, we might have an action that says "Home page has been loaded". It might mean some changes in the state. For example, in our application, it might trigger an API call for lists of expenses and incomes, which will in turn trigger an event that outs that data in the `Store`, resulting ina change in the UI. Or we might have an action that says "Add a category", which will create a new category of income/expense in the `Store`, again resulting in a UI change. Again, essentially `Actions` are like commands to the `Store`, or methods that allow to update its contents.
5+
`Actions` are the most simple core concept of NgRx (and Redux in general). Action is a unique event that is used to trigger a change in the state. What does it mean? For example, we might have an action that says "Home page has been loaded". It might mean some changes in the state. For example, in our application, it might trigger an API call for lists of expenses and incomes, which will in turn trigger an event that puts that data in the `Store`, resulting in a change in the UI. Or we might have an action that says "Add a category", which will create a new category of income/expense in the `Store`, again resulting in a UI change. Again, essentially `Actions` are like commands to the `Store`, or methods that allow to update its contents.
66

77
## What does an Action look like?
88

@@ -64,8 +64,4 @@ Yes, you've read it correctly: we have learned how to write some basic code in N
6464
> You will find solution code for all the homeworks in the end of the chapters
6565
> **Important!** Do not move to the next chapter without adding the homework code! We will be using that code in the next chapters
6666
67-
Example of a solution to the homework:
68-
69-
70-
7167
In this chapter, we learned how to create `Actions`, unique events that specify what should happen to the state. In the next one, we will be writing code that actually does the transformation in the state.

src/assets/content/chapters/chapter-7.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ import { reducer } from './state/reducer';
9494
// other metadata omitted
9595
imports: [
9696
// other imports omitted
97-
StoreModule,
9897
StoreModule.forRoot({categories: reducer}),
9998
],
10099
})

src/assets/content/chapters/chapter-8.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,27 @@ As promised, magic happens here. We already covered that in order to trigger a `
110110

111111
Awesome, right?
112112

113-
This diagram illustrates how this whole thing works:
113+
This diagram illustrates how the whole thing works:
114114

115+
![NgRx flow diagram](https://raw.githubusercontent.com/Armenvardanyan95/ngrx-tutorial/main/src/assets/content/images/store-diagram.png)
116+
117+
This showcases the whole process we described:
118+
119+
1. Component dispatches an `Action`
120+
2. `Reducer` takes the old `State` from the `Store` and the `Action`
121+
3. `Reducer` creates the new `State` and puts in the `Store`
122+
4. `Store` triggers the `Selectors`
123+
5. `Selector` emits the new derived `State` to the component via an `Observable`
124+
6. Component displays the new data with the `async` pipe
125+
126+
And that's it
127+
128+
## Homework
129+
130+
We created a flow in which the user can add a category. Now it is time for you to add a low in which the user can delete one. We already have the `Action` responsible for this and the handler in the `Reducer`, so go on and
131+
132+
1. Add a delete button in front of every category item in the UI, which will emit a new `categoryDeleted` event to the container
133+
2. In the container, dispatch the `Action` that deletes the category and observe the results
134+
135+
We have come full circle now and can read, modify and select `State`. In the upcoming chapter, we will lay some groundwork so we can start interacting with remote APIs using NgRx.
115136

src/assets/content/chapters/chapter-9.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export class CategoryService {
7676
) {}
7777

7878
getCategories() {
79-
return this.http.get<Category>(this.baseUrl);
79+
return this.http.get<Category[]>(this.baseUrl);
8080
}
8181

8282
getCategoryById(id: number) {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
### Homework example 1:
2+
```ts
3+
// in the category-list-presenter.ts file
4+
export class CategoriesEffects {
5+
deleteCategory$ = createEffect(() => this.actions$.pipe(
6+
ofType(deleteCategory),
7+
mergeMap(({payload}) => this.categoriesService.deleteCategory(payload).pipe(
8+
map(() => deleteCategorySuccess({payload})),
9+
catchError(() => of(loadCategoriesError())),
10+
))
11+
));
12+
}
13+
```
14+
15+
### Homework example 2:
16+
```ts
17+
export class CategoriesEffects {
18+
addCategory$ = createEffect(() => this.actions$.pipe(
19+
ofType(addCategory),
20+
mergeMap(({payload}) => this.categoriesService.addCategory(payload).pipe(
21+
map((result) => addCategorySuccess({payload: result})),
22+
catchError(() => of(addCategoryError())),
23+
)),
24+
));
25+
}
26+
```

0 commit comments

Comments
 (0)