Log In

Angular - @ngrx/effects - @ngrx/entity - @ngrx/schematics


by Yariv Katz

In this article we are going to learn about some packages released by ngrx. We already covered the most popular package for combining redux and angular. This package is called @ngrx/store and I recommend our readers to first go over the @ngrx/store article. So as mentioned we are going to go over the other popular packages released by @ngrx and when will we use those packages. The docs for those packages can be found in this link. So lets begin...

@ngrx/effects

The idea of effects is to decouple dispatching actions and reacting to redux actions from our components. Using effects we can react to actions in services and dispatch new actions based on previous ones. With effects we can more easily create pure components who simply react to state change and are not connected to actions. Lets try to demonsrate the effects usage on the following angular redux application. Our application will have a redux state, containing a list of todo items in the state. we will have a single app component which will be connected to that state and display a list of todo items. The todo items in the state will be populated from our rest todo api located at the following url: https://nztodo.herokuapp.com/api/task/?format=json now our immediate instict will be to make the api call on the ngOnInit lifecycle of the app component. Using effects we would like to decouple entirely the logic of fetching the todos and populating the state from our component. We would like fetching the todo items and populating the state made entirely from a TodoService we will create.

Lets start by creating a new angular application. In your terminal type:

> npx @angular/cli new ngrx-tutorial

@ngrx/store

@ngrx/effects is working together with @ngrx/store on an angular + redux application. So to learn how to use effects we will have to combine angular with @ngrx/store. Lets install @ngrx/store, in the terminal type:

> npm install @ngrx/store --save

Lets start by creating our Todo class model. In the src/app folder, create a folder called todo and inside that folder, create a new file called todo.ts our todo model contains just a title and a primary key id, place the following code in that file:

export class Todo {
    public title: string;
    public id: number;
    constructor(json) {
        this.title = json.title;
        this.id = json.id;
    }
}

Now lets create our actions. Our state contain a loading flag that will be set to true when the request to the server is sent and back to false when the request from server is returned. Our state will also contain an error that will be filled if the request is failed. Our state will also contain an array of the todo models that we taken from the server. In the todo folder, add a file called todo-actions.ts with the following:

import {Action} from '@ngrx/store';
import { Todo } from './todo';

export class SetLoading implements Action {
    static TYPE = '[Todo] SetLoading';
    type = SetLoading.TYPE;
    constructor(public payload : boolean) {}
}

export class SetError implements Action {
    static TYPE = '[Todo] SetError';
    type = SetError.TYPE;
    constructor(public payload : Error) {}
}

export class SetTodos implements Action {
    static TYPE = '[Todo] SetTodos';
    type = SetTodos.TYPE;
    constructor(public payload: Todo[]) {}
}

export type TodoActions = SetTodos | SetError | SetLoading;

We defined 3 actions as classes that implement the Action interface from @ngrx/store. The name of the action we defined as a static member in each class called TYPE. we exported the types of the classes and we recieve the payload of each action in the constructor.

Time to create our reducer. We will define the reducer and the reducer map in a file called todo-reducer.ts that will be placed in the todo folder.

import {ActionReducerMap, ActionReducer, Action} from '@ngrx/store';
import { Todo } from './todo';
import { TodoActions, SetLoading, SetError, SetTodos } from './todo-actions';

export interface IStateTodo {
    todos: Todo[],
    isLoading: boolean,
    error: Error
}

export interface IState {
    todo: IStateTodo;
}

const initialState : IStateTodo = {
    todos: [],
    isLoading: false,
    error: null
}

const todoReducer: ActionReducer<IStateTodo, TodoActions>  = function(state: IStateTodo = initialState, action: TodoActions): IStateTodo {
    switch(action.type) {
        case SetLoading.TYPE:
            return {
                ...state,
                isLoading: (<SetLoading>action).payload
            };
        case SetError.TYPE:
            return {
                ...state,
                error: (<SetError>action).payload
            };
        case SetTodos.TYPE:
            return {
                ...state,
                todos: (<SetTodos>action).payload
            };
        default:
            return state;
    }
}

export const todoReducers : ActionReducerMap<IState, Action> = {
    todo: todoReducer
}

each reducer is in charge of a chunk in our state. So we created a reducer and assigned it to a chunk in our reducer map called todo.

We need to add the @ngrx/store module to module and to assign it with the reducer map we created. Modify the app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import {StoreModule} from '@ngrx/store';
import { todoReducers } from './todo/todo-reducer';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
    declarations: [
        AppComponent
    ],
    imports: [
        BrowserModule,
        HttpClientModule,
        StoreModule.forRoot(todoReducers)
    ],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppModule { }

We simply added the StoreModule as well as the HttpClientModule to our imports array.

Lets connect our app component to the redux state we created. Modify the app.component.ts as follows.

import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { IState } from './todo/todo-reducer';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Todo } from './todo/todo';
import { HttpClient } from '@angular/common/http';
import { SetLoading, SetTodos, SetError } from './todo/todo-actions';

@Component({
    selector: 'app-root',
    template: `
        <h1>
            Todo tasks
        </h1>
        <ul>
            <li *ngFor="let task of todos$ | async">
                {{task.title}}
            </li>
        </ul>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent implements OnInit {
    todos$ : Observable<Todo[]>;

    constructor(
        private _store: Store<IState>,
        private _http: HttpClient
    ) {
        this.todos$ = _store.pipe(
        select('todo', 'todos')
        )
    }

    ngOnInit() {
        this._store.dispatch(new SetLoading(true));
        this._http.get('https://nztodo.herokuapp.com/api/task/?format=json').pipe(
            map((jsonArray: any) => jsonArray.map((singleJson) => new Todo(singleJson)))
        ).subscribe((todos: Todo[]) => {
            this._store.dispatch(new SetTodos(todos));
            this._store.dispatch(new SetLoading(false));
        }, (err) => {
            this._store.dispatch(new SetLoading(false));
            this._store.dispatch(new SetError(err));
        })
    }
}

So our component simply displays the list of todo tasks from the state. Notice the login in the OnInit, we are creating a get request and setting the state accordingly. launching your app you should see your app is working and fetching the todos and displaying the list.

@ngrx/effects

Lets take a look again in our app component, specificalldy in the OnInit lifecycle hook. Lets imagine we have another component that requires the entire list of todo tasks, or even better lets imagine 100 of those components. Can we really say which component has the responsibility of populating the state? Also we would very much benefit if those components will be as pure as possible, this means they only depend on the state and not in charge of populating the state. Think about the time when you will need to test those components, how easy it is to test your components usually means how good your app is built. When we test the components it will be easier if we just populate the state and look how the component suppose to look instead of also having to take care of populating the state from another component. @ngrx/effects allows us to decouple the action logic from the component. Instead we will place that logic in a service. Using npm we will install @ngrx/effects:

> npm install @ngrx/effects --save

In the todo folder create the file todo.service.ts with the following:

import {Injectable} from '@angular/core';
import { Actions, ofType, ROOT_EFFECTS_INIT, Effect, OnInitEffects } from '@ngrx/effects';
import { map, mergeMap, catchError } from 'rxjs/operators';
import { SetLoading, SetTodos, SetError } from './todo-actions';
import { HttpClient } from '@angular/common/http';
import { Todo } from './todo';
import { of } from 'rxjs';
import { Action } from '@ngrx/store';

@Injectable({
    providedIn: 'root'
})
export class TodoService implements OnInitEffects {
    
    @Effect()
    initLoading$ = this._actions$.pipe(
        ofType(SetTodos.TYPE),
        map(() => new SetLoading(false))
    )

    @Effect()
    initTodos$ = this._actions$.pipe(
        ofType(ROOT_EFFECTS_INIT),
        mergeMap(() => this._http.get('https://nztodo.herokuapp.com/api/task/?format=json')),
        map((jsonArray: any) => jsonArray.map((singleJson) => new Todo(singleJson))),
        map((todos: Todo[]) => new SetTodos(todos)),
        catchError((err) => of(new SetError(err)))
    )

    ngrxOnInitEffects(): Action {
        return new SetLoading(true);
    }

    constructor(
        private _actions$ : Actions,
        private _http: HttpClient
    ) {}
}

Few things to note here:
- We are injecting the Actions service from @ngrx/effects which is an observable emitting every action that happens.
- We can use the ofType selector, to select an action of certain type
- the ROOT_EFFECTS_INIT will be called after our root effects have been added.
- OnInitEffects interface makes us implement the method ngrxOnInitEffects which can return an action after the effects has been initiated.
- When placing the @Effect decorator you will have to return an observable of a new action from previous action.
- The first effect decorator will listen for the SetTodo action and return a new action of SetLoading false
- The second decorator will listen for the init effects action and return SetTodos if request is successful or SetError if the request failed.

You can now modify the app component and remove the on init method. you app component is now decoupled from the actions and only react to state change. That component is much more pure and easier to test. You can run your app and you should see the list of todos populated and displayed.

To summerise @ngrx/effects let us create special services with the @Effect decorator. @Effect decorator lets us decorate an observable that will contain an action. That observable usually (not a must) reacts to other actions. Other that the use case demonstrated of returning an action from another you can log the actions that happens in your app, you can save them and easier preform things like undo.

@ngrx/store-devtools

When working with redux it becomes essential to inspect our state from now and again. For example we navigate through our app and discover a bug, we would like to know the current state so we can write a test for that bug. To be able to examine our store when needed we can use @ngrx/store-devtools. This will connect our store to the browser extension Redux Devtools Lets start with installing the package:

> npm install @ngrx/store-devtools --save

Make sure you installed the extension in your browser. Now in the app.module.ts we would have to modify the module imports to include the dev tool instrument.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import {StoreModule} from '@ngrx/store';
import { todoReducers } from './todo/todo-reducer';
import { HttpClientModule } from '@angular/common/http';
import {EffectsModule} from '@ngrx/effects';
import { TodoService } from './todo/todo.service';
import {StoreDevtoolsModule} from '@ngrx/store-devtools';

@NgModule({
    declarations: [
        AppComponent
    ],
    imports: [
        BrowserModule,
        HttpClientModule,
        StoreModule.forRoot(todoReducers),
        EffectsModule.forRoot([TodoService]),
        StoreDevtoolsModule.instrument()
    ],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppModule { }

If you installed the extension you should run your app now and see the extension light up and when clicked you can view in the dialog the current state and the actions that led to that state.

So @ngrx/store-devtools allows us to debug our store, view the state at any given time and the actions that led to this state.

@ngrx/router-store

With this package we can connect routes change to our state. This means that we can leverage the ngrx tools and the state to handle route actions. Lets try to demonstrate this package with a small example. In this example we will add another component called search.component.ts. This component will contain a form with a search input, as we type along the url will change and we will add a query param: ?search=our-search-term We want this to cause a request to the server with that query param, meaning if we send our server a request to the url: https://nztodo.herokuapp.com/api/task/?format=json&search=our-search-term the server will returned a filtered todo items and we will populate our list of items with the filtered result. To achieve this we will use the router store package combined with our service and effects we created earlier.

Lets start by installing the router-store using npm:

> npm install @ngrx/router-store --save

To update the state with router events, we will need to add the reducer from @ngrx/router-store to our reducer map. Modify the file: todo-reducer.ts as follows:

import {ActionReducerMap, ActionReducer, Action} from '@ngrx/store';
import { Todo } from './todo';
import { TodoActions, SetLoading, SetError, SetTodos } from './todo-actions';
import { routerReducer, RouterReducerState } from '@ngrx/router-store';

export interface IStateTodo {
    todos: Todo[],
    isLoading: boolean,
    error: Error
}

export interface IState {
    todo: IStateTodo;
    router: RouterReducerState;
}

const initialState : IStateTodo = {
    todos: [],
    isLoading: false,
    error: null
}

const todoReducer: ActionReducer<IStateTodo, TodoActions>  = function(state: IStateTodo = initialState, action: TodoActions): IStateTodo {
    switch(action.type) {
        case SetLoading.TYPE:
            return {
                ...state,
                isLoading: (<SetLoading>action).payload
            };
        case SetError.TYPE:
            return {
                ...state,
                error: (<SetError>action).payload
            };
        case SetTodos.TYPE:
            return {
                ...state,
                todos: (<SetTodos>action).payload
            };
        default:
            return state;
    }
}

export const todoReducers : ActionReducerMap<IState, Action> = {
    todo: todoReducer,
    router: routerReducer
}

Our state is a bit changed and we have another router section that the routerReducer is in charge of.

In addition we will have to modify the file app.module.ts where we will have to add the RouterModule and also add StoreRoutingConnectingModule which is in charge of connecting the router with our store. Modify app.module.ts:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import {StoreModule} from '@ngrx/store';
import { todoReducers } from './todo/todo-reducer';
import { HttpClientModule } from '@angular/common/http';
import {EffectsModule} from '@ngrx/effects';
import { TodoService } from './todo/todo.service';
import {StoreDevtoolsModule} from '@ngrx/store-devtools';
import { RouterModule } from '@angular/router';
import { StoreRouterConnectingModule } from '@ngrx/router-store';

@NgModule({
    declarations: [
        AppComponent
    ],
    imports: [
        BrowserModule,
        HttpClientModule,
        StoreModule.forRoot(todoReducers),
        EffectsModule.forRoot([TodoService]),
        StoreDevtoolsModule.instrument(),
        RouterModule.forRoot([]),
        StoreRouterConnectingModule.forRoot()
    ],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppModule { }

Now if you run your app now and view the state through the redux dev tools extension you will see another section in the state named router. Time to create our search component. In the terminal type:

> ng g c Search --inline-template

This command will create a folder search and the file search.component.ts in that folder. modify the file search.component.ts as follows:

import { Component, ChangeDetectionStrategy } from '@angular/core';
import { Router } from '@angular/router';

@Component({
    selector: 'app-search',
    template: `
        <form>
            <div class="form-group">
                <label>Search</label>
                <input 
                type="search" 
                class="form-control" 
                (input)="navigate($event)"
                name="search" />
            </div>
        </form>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SearchComponent {

    constructor(private _router : Router) { }

    navigate = (event) => {
        const search = event.target.value;
        this._router.navigateByUrl(`/?search=${search}`);
    }

}

When typing text in the search input we activate the navigate event which will direct us to the same url we are in only with a search query param. Lets add our component to the app by adding the selector in the app.component.ts

import { Component, ChangeDetectionStrategy } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { IState } from './todo/todo-reducer';
import { Observable } from 'rxjs';
import { Todo } from './todo/todo';
import { HttpClient } from '@angular/common/http';

@Component({
    selector: 'app-root',
    template: `
        <h1>
            Todo tasks
        </h1>
        <app-search></app-search>
        <ul>
            <li *ngFor="let task of todos$ | async">
                {{task.title}}
            </li>
        </ul>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
    todos$ : Observable<Todo[]>;

    constructor(
        private _store: Store<IState>,
        private _http: HttpClient
    ) {
        this.todos$ = _store.pipe(
        select('todo', 'todos')
        )
    }
}

If you run your app now and type text in the search you will notice that the url is changing with the query param. Activating the dev tools extension you will also notice that each navigation triggers an action and updates the router state with the query params. This means that we can modify our todo service effect to react to those actions and populate the todo array in the state. Modify the todo.service.ts as follows:

import {Injectable} from '@angular/core';
import { Actions, ofType, ROOT_EFFECTS_INIT, Effect, OnInitEffects } from '@ngrx/effects';
import { map, mergeMap, catchError, debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { SetLoading, SetTodos, SetError } from './todo-actions';
import { HttpClient } from '@angular/common/http';
import { Todo } from './todo';
import { of } from 'rxjs';
import { Action } from '@ngrx/store';
import {ROUTER_NAVIGATION} from '@ngrx/router-store';

@Injectable({
    providedIn: 'root'
})
export class TodoService implements OnInitEffects {
    
    @Effect()
    initLoading$ = this._actions$.pipe(
        ofType(SetTodos.TYPE),
        map(() => new SetLoading(false))
    )

    @Effect()
    initTodos$ = this._actions$.pipe(
        ofType(ROUTER_NAVIGATION),
        debounceTime(1000),
        map((action: any) => action.payload.routerState.root.queryParams.search || ''),
        distinctUntilChanged(),
        mergeMap((search) => this._http.get(`https://nztodo.herokuapp.com/api/task/?format=json&search=${search}`)),
        map((jsonArray: any) => jsonArray.map((singleJson) => new Todo(singleJson))),
        map((todos: Todo[]) => new SetTodos(todos)),
        catchError((err) => of(new SetError(err)))
    )

    ngrxOnInitEffects(): Action {
        return new SetLoading(true);
    }

    constructor(
        private _actions$ : Actions,
        private _http: HttpClient
    ) {}
}

We changed the initTodos$ Effect. that effect will be incharge of fetching the todo items. The effect is listening for a navigation change, we wait 1 sec to not submit a request to the server every user type rather wait 1 sec after he finished typing. We then grabbing the search query param from the action. We take that search string and send a request to the server and populate the todo elements.

Notice the complete isolation between the search component and the app component, they are not coupled the search is only changing the url everything else is done through effect.

So the @ngrx/router-store helps us make the router part of our redux state.

@ngrx/entity

The next ngrx package we will cover is @ngrx/entity. @ngrx/entity helps us manage collection in our state. It helps us minimize the boilerplate code in the reducer for managing the collections in our state. It is also helpful to look at the entity library as a guideline to how to arrange the collections in our state. Looking at our current state we have a collection of todo tasks that we grabbed from the server. Lets try to manage that collection using @ngrx/entity.

First lets install @ngrx/entity using npm:

> npm install @ngrx/entity --save

the entity package helps us with boilerplate code in our reducer for dealing with collection, so the usage of the library will mainly be in our reducer files.

the EntityState is an interface that guides us how to arrange our collection state. In our case it will help us arrange the todo state. The EntityState contains an array of ids which will contain an array of the primary keys of our todo tasks. In our case the todo tasks primary key is the property id. the EntityState also contains an entities map which is an object arranged by the pk containing in each pk the Todo task with the same id. This means that our IStateTodo needs to change and extend that interface. Modify the file todo-reducers.ts and change the part of the IStateTodo

import {EntityState} from '@ngrx/entity';

export interface IStateTodo extends EntityState<Todo> {
    isLoading: boolean,
    error: Error
}

Next we need to create our adapter. the adapter gives us common functions and selectors to deal with our entity collection. For example we can use the adapter to add an item to our collection or multiple items or perform CRUD operations. The adapter can also expose selectors to select certain data from our collection.

We can also use the adapter to create our initialState. to create the initialState we only need now to provide the keys that are not part of EntityState which in our case is isLoading, error In the file todo-reducers.ts update the part of the initialState const to this:

...
import {EntityState, createEntityAdapter} from '@ngrx/entity';

export const adapter = createEntityAdapter<Todo>();

const initialState : IStateTodo = adapter.getInitialState({
    isLoading: false,
    error: null
})

Now we can use the adapter to also help us in the reducer function. For example we have the action SetTodos which adds multiple items to our collection. In the file todo-reducer.ts in the switch case of the SetTodos action change to the following:

case SetTodos.TYPE:
    return adapter.addAll((<SetTodos>action).payload, state);

So having a collection manage with the adapters will mean that alot of the reducer cases will now be a single line using the adapter.

Summary

To summerise...
@ngrx/store is the most popular redux library for angular.
If you decide to go with redux and angular we recommend going for @ngrx/store.
And if using the @ngrx/store there are a bunch of useful tools that the ngrx team published which helps us remove sideeffects by using Effects and listen to certain actions and emitother actions. Or we can use @ngrx/router-store if our app contains navigation, to add navigation actions to our store. Or we can use Entities to manage collections in our store. Combining those additional tools is essential for our redux application.
Happy coding...