|
| 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. |
0 commit comments