487 lines
15 KiB
Markdown
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/>
|