nx/docs/guides/misc-ngrx.md
2019-02-20 16:07:58 -05:00

487 lines
15 KiB
Markdown

# Setting Up NgRx
Leveraging [NgRx](https://github.com/ngrx/platform) for state management in an Angular application involves
- planning out the root state and any feature states.
- setup of files for NgRx actions, reducers, selectors, and effects.
- preparation of testing `*.spec.*` files.
- updates to library or application modules to register the NgRx services
> Did you know that this collection of generated files used to manage the NgRx state are often referred to as 'NgRx' boilerplate?
## Overview
You use this Nx schematic to build out a new NgRx feature area that provides a new slice of managed state.
**@nrwl/schematics** has an `ngrx` schematic to generate files that implement best patterns for NgRx scaffolding. This schematic generates source files and then enhances the generated NgRx boilerplate with Nx improvements.
The `ngrx` schematic generates a NgRx feature set containing the following files:
- `actions`,
- `reducer`,
- `effects`,
- `selectors`, and
- `facade` (optional)
## Terminal Command
**`ngrx`** _<name>   --module=""   [options]_
> Note: the `name` and the `--module=` arguments are required!
You can generate new **feature** state (NgRx files) which are registered with the `StoreModule.forFeature()` in the feature library ngModule OR the `StoreModule.forRoot()` in the application ngModule.
> Feature state libraries can be lazy loaded and support feature state slices that are independent of other feature states.
```bash
ng generate ngrx <FeatureName> --module="" [options]
ng g ngrx <FeatureName> --module="" [options]
```
Before you start generating your files, let's first review the schematic command options:
## ngrx Command Options
- `name` : Specifies the name of the NgRx feature (required)
- `module` : Specifies the parent directory for the NgRx folder (required)
* `facade` : Specifies to generate an associated Facade class with the NgRx files
* `directory` : Specifies the name of the grouping folder for the NgRx files
* `root` : Add StoreModule.forRoot and EffectsModule.forRoot instead of forFeature
* `onlyAddFiles` : Only add new NgRx files, without changing the module file
* `onlyEmptyRoot` : Do not generate any files. Only generate StoreModule.forRoot and EffectsModule.forRoot
* `skipPackageJson` : Do not add NgRx dependencies to package.json
Let's first walk through how we can use Nx `ngrx` schematic to get started with NgRx in an Angular application by setting up the root level store and corresponding files. Then we will take a look at how we can add feature level store segments as our application grows, ensuring that our code follows a common pattern each time.
<br/>
#### 1) `name`
Specifies the name of the NgRx feature (e.g., Products, Users, etc.).
- `name`
- Type: `string`
- Required: true
* Do not use `State` a suffix.
* We recommend developers use the plural forms for feature 'name'; e.g. Products, Users, Cars, etc.
```bash
ng g ngrx <FeatureName> [options]
```
#### 2) `module`
Specifies the path to Angular `ngModule`. This option is **always** required and is used to determine the **parent directory** for the new **+state** folder.
- `--module`
- Type: `string`
- Required: true
```bash
ng g ngrx <FeatureName> --module=<xxx> [options]
```
- Another option can specify an application root module when the `--root` is specified. The NgRx state files are registered with the `StoreModule.forRoot()` in the application module.
> e.g. --module=apps/myapp/src/app/app.module.ts
- Otherwise, this option can specify a library module. The parent folder to this module will also be used as the _container_ library for the new NgRx state files. Consider the following example of a feature library `state` used for _comments_... organized within a _comments_ grouping folder.
> e.g. --module=libs/comments/state/src/lib/comments-state.module.ts
#### 3) `facade`
Specify this flag to generate NgRx Facade class(es) along with the standard NgRx scaffolding.
- `--facade`
- Type: `boolean`
- Required: false; defaults to `false`
```bash
ng g ngrx <FeatureName> -module=<xxx> --facade [options]
```
> See the blog [Better State Management with Facades](https://blog.nrwl.io/nrwl-nx-6-2-angular-6-1-and-better-state-management-e139da2cd074#cb93) for details.
#### 4) `directory`
Specifies the name of the grouping folder used to contain the feature ngrx files: `<feature>.reducer.ts`, `<feature>.effects.ts`, `<feature>.selectors.ts`, `<feature>.actions.ts`. If not specified, a default folder named `+state` will be used to group the files.
> Since this `+state` folder is within a library folder, the required `--module` option indicates **which** library will contain the new state files.
- `--directory`
- Type: `string`
- Default: `+state`
<br/>
---
### Root
Making use of the Angular CLI ng generate command, we can use the ngrx schematic to scaffold out the following in our applications:
- Root level NgRx configuration
- Feature level NgRx configuration
##### Option) `root`
Getting up and running with ngrx starts with creating a store at the root level of the application.
We can run the generate command for ngrx with the module and root options to create a new root level store and corresponding pieces needed:
- `--root`
- Type: `boolean`
- Required: false; defaults to `false`
```bash
ng generate ngrx app --module=apps/<appname>/src/app/app.module.ts --root
```
We will see the following files created:
```console
apps/<appname>/src/app/+state/app.actions.ts
apps/<appname>/src/app/+state/app.effects.ts
apps/<appname>/src/app/+state/app.effects.spec.ts
apps/<appname>/src/app/+state/app.reducer.ts
apps/<appname>/src/app/+state/app.reducer.spec.ts
```
Also, app.module.ts will have StoreModule.forRoot and EffectsModule.forRoot configured.
##### Option) `onlyEmptyRoot`
We can run the generate command for ngrx with the module and onlyEmptyRoot option to only add the StoreModule.forRoot and EffectsModule.forRoot calls without generating any new files.
```bash
ng generate ngrx app --module=apps/<appname>/src/app/app.module.ts --onlyEmptyRoot
```
This can be useful in the cases where we don't have a need for any state at the root (or app) level.
### Feature
We can run the generate command for ngrx with the module option to create a new feature level store and corresponding pieces needed:
```bash
ng generate ngrx products --module=libs/<libname>/src/mymodule.module.ts
```
We will see the following files created:
```console
libs/<libname>/src/+state/products.actions.ts
libs/<libname>/src/+state/products.effects.ts
libs/<libname>/src/+state/products.effects.spec.ts
libs/<libname>/src/+state/products.reducer.ts
libs/<libname>/src/+state/products.reducer.spec.ts
```
Also, mymodule.module.ts will have StoreModule.forFeature and EffectsModule.forFeature configured
#### Option) `onlyAddFiles`
We can run the generate command for ngrx with the module and `--onlyAddFiles` option to generate files without adding imports to the module.
```console
ng generate ngrx products --module=apps/<appname>/src/app/mymodule/mymodule.module.ts --onlyAddFiles
```
This can be useful when we want to start building out our state without wiring it up to our Angular application yet.
<br/>
----
## Learn by Example
Consider a command to generate a `Comments` NgRx feature set and register it within an application root ngModule.
```bash
ng generate NgRx Comments --root --module=apps/myapp/src/app/app.module.ts
```
> This would use `StoreModule.forRoot()` to register the Comments NgRx state functionality.
<br/>
Better yet, let's generate a `Comments` feature set within a `state` library and register it with the `comments-state.module.ts` file in the same `comments/state` folder.
```bash
ng g ngrx Comments --module=libs/comments/state/src/lib/comments-state.module.ts
```
#### Generated Files
The files generated are shown below and include placeholders for the _comments_ state.
> The Comments notation used be below indicates a placeholder for the actual _comments_ name.
- [comments.actions.ts](#commentsactionsts)
- [comments.reducer.ts](#commentsreducerts)
- [comments.selectors.ts](#commentsselectorsts)
- [comments.effects.ts](#commentseffectsts)
- [../app.module.ts](#appmodulets) or [../comments-state.module.ts](#commentsstatemodulets)
<br/>
###### comments.actions.ts
```typescript
import {Action} from "@ngrx/store";
export enum CommentsActionTypes {
LoadComments = "[Comments] Load Comments",
CommentsLoaded = "[Comments] Comments Loaded"
CommentsLoadError = "[Comments] Comments Load Error"
}
export class LoadComments implements Action {
readonly type = CommentsActionTypes.LoadComments;
}
export class CommentsLoadError implements Action {
readonly type = CommentsActionTypes.LoadComments;
}
export class CommentsLoaded implements Action {
readonly type = CommentsActionTypes.CommentsLoaded;
constructor(public payload: any[]) { }
}
export type CommentsAction = LoadComments | CommentsLoaded | CommentsLoadError;
export const fromCommentsActions = {
LoadComments,
CommentsLoaded,
CommentsLoadError
}
```
###### comments.selectors.ts
```typescript
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { CommentsState } from './comments.reducer';
const getCommentsState = createFeatureSelector<FeatureState>('CommentsState');
const getLoaded = createSelector( getCommentsState, (state:CommentsState) => return state.loaded );
const getSelectedId = createSelector( getCommentsState, (state:CommentsState) => return state.selectedId );
const getAllComments = createSelector( getCommentsState, getLoaded, (state:CommentsState, isLoaded) => {
return isLoaded ? state.list : [ ];
});
const getSelectedComments = createSelector( getAllComments, getSelectedId, (list, id) => {
let comments = list.find(it => it.id == id);
return comments ? Object.assign({}, comments) : undefined;
});
export const commentsQuery = {
getLoaded,
getAllComments,
getSelectedComments
}
```
###### comments.reducer.ts
```typescript
import { CommentsAction, CommentsActionTypes } from './comments.actions';
import { Comments, CommentsState } from './comments.reducer';
/**
* Interface for the 'Comments' data used in
* - CommentsState, and
* - commentsReducer
*/
export interface Entity {}
export interface CommentsState {
list: Entity[]; // analogous to a sql normalized table
loaded: boolean; // has the Comments list been loaded ?
selectId?: string | number; // which Comments record has been selected
error?: any; // last none error (if any)
}
export const initialState: CommentsState = {
list: [],
loaded: false
};
export function commentsReducer(
state: CommentsState = initialState,
action: CommentsAction
): CommentsState {
switch (action.type) {
case CommentsActionTypes.CommentsLoaded: {
state = {
...state,
list: action.payload,
loaded: true
};
break;
}
}
return state;
}
```
###### comments.effects.ts
```typescript
import { Injectable } from '@angular/core';
import { Effect, Actions } from '@ngrx/effects';
import { DataPersistence } from '@nrwl/nx';
import { CommentsState } from './comments.reducer';
import {
CommentsLoadError,
CommentsLoaded,
CommentsActionTypes
} from './comments.actions';
@Injectable()
export class CommentsEffects {
@Effect()
loadComments$ = this.dataPersistence.fetch(CommentsActionTypes.LoadComments, {
run: (action: LoadComments, state: CommentsState) => {
// Your custom REST 'load' logic goes here. For now just return an empty list...
return new CommentsLoaded([]);
},
onError: (action: LoadComments, error) => {
console.error('Error', error);
return new CommentsLoadError(error);
}
});
constructor(
private actions: Actions,
private dataPersistence: DataPersistenceComments
) {}
}
```
<br/>
---
#### Registering your NgRx state as _Root_
If you are register the Comments NgRx as part of the `.forRoot()` state, then:
e.g.
```bash
ng generate ngrx Comments --root --module=apps/myapp/src/app/app.module.ts
```
will update the root ngModule with NgRx configurations:
<br/>
###### apps/myapp/src/app/app.module.ts
```typescript
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { environment } from '../environments/environment';
import { StoreRouterConnectingModule } from '@ngrx/router-store';
import { storeFreeze } from 'NgRx-store-freeze';
import {
commentsReducer,
CommentsState,
initialState,
CommentsEffects
} from '<npmScope>/comments';
@NgModule({
imports: [
BrowserModule,
RouterModule.forRoot([]),
StoreModule.forRoot(
{ comments: commentsReducer },
{
initialState: { comments: commentsInitialState },
metaReducers: !environment.production ? [storeFreeze] : []
}
),
EffectsModule.forRoot([CommentsEffects]),
!environment.production ? StoreDevtoolsModule.instrument() : [],
StoreRouterConnectingModule
],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule {}
```
<br/>
#### Registering your NgRx state as _Feature_
Otherwise you are registering your Comments state management as a feature library. This is the recommended approach.
The command:
```bash
ng g ngrx Comments --module=libs/comments/state/src/lib/comments-state.module.ts
```
which will update the feature library ngModule with NgRx Comments configurations as follows:
<br/>
###### libs/comments/state/src/lib/comments-state.module.ts
```typescript
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { initialState, commentsReducer } from './+state/comments.reducer';
import { CommentsEffects } from './+state/comments.effects';
@NgModule({
imports: [
CommonModule,
StoreModule.forFeature('comments', commentsReducer, { initialState }),
EffectsModule.forFeature([CommentsEffects])
]
})
export class CommentsStateModule {}
```
<br/>
#### Exporting the Public API
Finally, we update the <Feature> library's barrel `index.ts` to export the updated _public API_:
- the NgRx queries (aka selectors),
- the NgRx feature reducer
- the NgRx feature ngModule
<br/>
###### libs/comments/comments-state/src/lib/index.ts
```typescript
export * from './lib/+state/comments.selectors';
export * from './lib/+state/comments.reducer';
export { CommentsStateModule } from './lib/comments-state.module';
```
<br/>