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