How to use ngrx/store with Ionic 4

Using ngrx in ionic 4 is just as simple as using ngrx in angular application. Don’t worry if you are not familiar with it. In this tutorial, I’ll explain to you how to use ngrx in ionic 4 application.

We are going to create a very basic Todo Application to demonstrate the structure of ngrx in ionic 4 application.

At the end of this tutorial, we’ll be having an ionic 4 application which is having one page called todo-list which shows the list of todos using ngrx-store.

How to use ngrx/store with Ionic 4:


STEP 1: CREATE IONIC 4 PROJECT

First of all, we need a basic ionic 4 application.

If you don’t know how to create ionic 4 application, I would recommend reading my previous article: Ionic 4 For Beginners


STEP 2: GENERATE PAGES

We need one page to show the todo list, so we are going to create one page for it.
you can create a page in ionic by running the following command:  


ionic generate page pages/todo-list

If you look into the above command, we’ve written pages/todo, that means we are telling ionic to generate todo-list page inside pages folder. and it generates the page with lazy loading feature.

It will create a todo-list folder in the project as follows:

ionic 4 generate page

If you’ll notice the todo-list folder, it comes up with todo-list.module.ts. This is going to support lazy loading feature.

When you’ll generate the page in ionic 4, all the generated page will be having its own module and its lazy-loading router configuration will be updated automatically when you’ll generate the page,

so app-routing.module.ts page will be having the following line in routes array:


import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  {
    path: '',
    redirectTo: 'home',
    pathMatch: 'full'
  },
  {
    path: 'home',
    loadChildren: './pages/home/home.module#HomePageModule'
  },
  {
    path: 'list',
    loadChildren: './pages/list/list.module#ListPageModule'
  },
  { path: 'todo', loadChildren: './pages/todo/todo.module#TodoPageModule' },
  { path: 'todo-list', loadChildren: './pages/todo-list/todo-list.module#TodoListPageModule' }
];
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

which tell that when /todo-list is requested then load TodoListPageModule as lazy loaded module


STEP 3: INSTALL NGRX

Once you have a basic setup for ionic application, then install ngrx packages.  


npm install --save @ngrx/store @ngrx/effects

NOTE:  Here I am using in-memory web api, because it emulates CRUD operations over REST API
You can learn more about in memory web api here. so the following package is not compulsory to install.


npm install --save angular-in-memory-web-api

STEP 4: GENERATE SERVICE

The good practices to create services is inside its own module called CoreModule.

Here in CoreModule, we can create services, guards, interceptors, models and etc.  

We’ll create CoreModule as following structure

STEP 4: GENERATE SERVICE

You can see that I’ve created services folder, you can create other folder based on your need i.e guards, interceptors etc.  

Now create todo.service.ts in the app/core/services directory and copy the following code into it.


import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { Observable, of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';

import { Todo } from '../../state/todo/todo.model';

@Injectable()
export class TodoService {
    private todoUrl = 'api/todos';
    constructor(private httpClient: HttpClient) { }

    getTodos(): Observable<Array<Todo>> {
        return this.httpClient.get<Todo[]>(this.todoUrl);
    }

    getTodo(id: number): Observable<Todo> {
        return this.getTodos().pipe(
            map(todos => todos.find(todo => todo.id === id))
        );
    }

    save(todo: Todo): Observable<Todo> {
        if (todo.id) {
            return this.put(todo);
        }
        return this.post(todo);
    }

    delete(todo: Todo): Observable<Todo> {
        const url = `${this.todoUrl}/${todo.id}`;

        return this.httpClient
            .delete<void>(url)
            .pipe(switchMap(() => of(todo)));
    }

    private post(todo: Todo): Observable<Todo> {
        return this.httpClient.post<Todo>(this.todoUrl, todo);
    }


    private put(todo: Todo): Observable<Todo> {
        const url = `${this.todoUrl}/${todo.id}`;

        return this.httpClient
            .put(url, todo)
            .pipe(switchMap(() => of(todo)));
    }
}

This service contains methods for getTodos (All todos), getTodo (todo By Id) and it gets data from in-memory web API because we are using in-memory web api in our example, but you can set up to get real data from live API endpoint.


STEP 5: In Memory DataService

We’ll create inmemory dataservicce, it’s not compulsory to create but since we are not using real api endpoint, we’ll use it to fake the api endpoint. Generate new file in-memory-data-service.ts and copy the following code into it.


import { Todo } from '../state/todo/todo.model';

export class InMemoryDataService {
    createDb() {
        const todos: Array<Todo> = [
            { id: 1, name: "Shopping" },
            { id: 2, name: "Meeting" }
        ];

        return { todos };
    }
}

STEP 6: Core Module

Now create a new file called core.module.ts.

Here we’ll import InMemoryWebApiModule and will define all services created in this module, In our case, we have created on one service called todo.service.ts

Copy the following code into core.module.ts


import { HttpClientModule } from '@angular/common/http';
import { CommonModule } from '@angular/common';
import {
    ModuleWithProviders,
    NgModule,
    Optional,
    SkipSelf
} from '@angular/core';

import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from '../core/in-memory-data.service';

import { TodoService } from './services/todo.service';

@NgModule({
    imports: [
        CommonModule,
        HttpClientModule,
        InMemoryWebApiModule.forRoot(InMemoryDataService, { delay: 600 })
    ],
    providers: [
        TodoService]
})
export class CoreModule {
    static forRoot(): ModuleWithProviders {
        return {
            ngModule: CoreModule
        };
    }

    constructor(
        @Optional()
        @SkipSelf()
        parentModule: CoreModule
    ) {
        if (parentModule) {
            throw new Error(
                'CoreModule is already loaded. Import it in the AppModule only'
            );
        }
    }
}

STEP 7: SETUP NGRX 

First, create a new folder called state in app folderthis folder is responsible for containing the state for all modules that the app contains.  

To easily distinguish the state of all modules, we’ll create a separate folder for each module inside state folder.  

In our case, we’ll create todo folder inside state folder so it will look like the following structure.

How to use ngrx/store with Ionic 4

Now we’ll create few files of ngrx setup in todo folder, so at the end, the folder will look like the following:

File structure

Ngrx Actions

At first, we are creating the string const variables as following:


export const GET_ALL_TODOS = '[TODO] Get All Todos';

We are exporting this const, because we will need these in the reducer and effects later.

We will use this const string to define the type of the actions.

Create todo.actions.ts file in app/state/todo directory, this file is responsible for creating actions which are related to todo module.

Here we’ve created few actions to get todo list from API. and there is a success and fail action which will be called from effects based on the response of API.  

Now copy the following code in your todo.action.ts  


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

export const GET_ALL_TODOS = '[TODO] Get All Todos';
export const GET_ALL_TODOS_SUCCESS = '[TODO] Get All Todos Success';
export const GET_ALL_TODOS_FAIL = '[TODO] Get All Todos Fail';

export const GET_TODO = '[TODO] Get Todo';
export const GET_TODO_SUCCESS = '[TODO] Get Todo Success';
export const GET_TODO_FAIL = '[TODO] Get Todo Fail';

//Get Todo List
export class GetAllTodos implements Action {
    readonly type = GET_ALL_TODOS;
}

export class GetAllTodosSuccess implements Action {
    readonly type = GET_ALL_TODOS_SUCCESS;
    constructor(public payload: Todo[]) { }
}

export class GetAllTodosFail implements Action {
    readonly type = GET_ALL_TODOS_FAIL;
    constructor(public payload: any) { }
}

//Get todo by id
export class GetTodo implements Action {
    readonly type = GET_TODO;
    constructor(public payload: number) { }
}

export class GetTodoSuccess implements Action {
    readonly type = GET_TODO_SUCCESS;
    constructor(public payload: Todo) { }
}

export class GetTodoFail implements Action {
    readonly type = GET_TODO_FAIL;
    constructor(public payload: any) { }
}

export type TodoActions =
    GetAllTodos
    | GetAllTodosSuccess
    | GetAllTodosFail
    | GetTodo
    | GetTodoSuccess
    | GetTodoFail;

Ngrx Reducers

Now copy the following code in your todo.reducer.ts


import * as fromTodo from "./todo.actions";
import { Todo } from './todo.model';

export interface State {
    todos: Todo[],
    loading: boolean;
    error: string;
}

export const initialState: State = {
    todos: [],
    loading: false,
    error: ''
};

export function reducer(state = initialState, action: fromTodo.TodoActions): State {
    switch (action.type) {

        case fromTodo.GET_ALL_TODOS: {
            return {
                ...state,
                loading: true
            };
        }

        case fromTodo.GET_ALL_TODOS_SUCCESS: {
            return {
                ...state,
                loading: false,
                todos:action.payload
            };
        }

        case fromTodo.GET_ALL_TODOS_FAIL: {
            return {
                ...state,
                loading: false,
                error: 'error loading todos'
            };
        }

        case fromTodo.GET_TODO: {
            return {
                ...state,
                loading: true
            };
        }

        case fromTodo.GET_TODO_SUCCESS: {
            return {
                ...state,
                loading: false
            };
        }

        case fromTodo.GET_TODO_FAIL: {
            return {
                ...state,
                loading: false,
                error: 'error loading todo'
            };
        }

        default: {
            return state;
        }
    }
}

export const getAllTodos = (state: State) => state.todos;
export const getLoading = (state: State) => state.loading;
export const getError = (state: State) => state.error;

Ngrx Effects

Effect will communicate with service. It will call service and based on the response it will dispatch the success or fail action.

If you’ll look into getAllTodos$ effect, you can see that it is calling todoservice method getTodos
and based on its response it will dispatch either GetAllTodoSuccess or GetAllTodosFail.

Copy the following code in your todo.effects.ts


import { Injectable } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects';
import { Action } from '@ngrx/store';
import { Observable, of } from 'rxjs';
import { map, switchMap, catchError } from 'rxjs/operators';


import { Todo } from './todo.model';
import * as TodoActions from './todo.actions';
import { TodoService } from '../../core/services/todo.service';

@Injectable()
export class TodosEffects {
    @Effect()
    getAllTodos$: Observable<Action> = this.actions$
        .ofType(TodoActions.GET_ALL_TODOS)
        .pipe(
        switchMap(() => this.todoService.getTodos().
            pipe(
            map((todos: Todo[]) => new TodoActions.GetAllTodosSuccess(todos)),
            catchError(err => of(new TodoActions.GetAllTodosFail(err)))
            ))
        );

    @Effect()
    getTodoById: Observable<Action> = this.actions$
        .ofType(TodoActions.GET_TODO)
        .pipe(
        map((action: TodoActions.GetTodo) => action.payload),
        switchMap((id) => this.todoService.getTodo(id).
            pipe(
            map((todo: Todo) => new TodoActions.GetTodoSuccess(todo)),
            catchError(err => of(new TodoActions.GetTodoFail(err)))
            ))
        );

    constructor(private actions$: Actions, private todoService: TodoService) { }
}

Ngrx Selector

The task of a selector is to select the property from the ngrx state, and we can call this selector from our component to fetch the data of ngrx state.

For example:


export const getAllTodos = createSelector(
    getTodosState,
    fromTodos.getAllTodos
  );

Here you can see that we are telling ngrx that select getAllTodos from getTodoState.

So It will call getAllTodos
selector which is defined in reducer.

Now copy the following code in your index.ts


import { createSelector, createFeatureSelector } from '@ngrx/store';
import * as fromTodos from './todo.reducer';
import { State as TodoState } from './todo.reducer';

export const getTodosState = createFeatureSelector<TodoState>('todo');

export const getAllTodos = createSelector(
    getTodosState,
    fromTodos.getAllTodos
  );

export const getLoading = createSelector(
  getTodosState,
  fromTodos.getLoading
);

export const getError = createSelector(
  getTodosState,
  fromTodos.getError
);

Todo Model

Generate todo model as following in your todo.model.ts


export interface Todo {
    id: number;
    name: string;
  }

Root State

Generate AppState interface to represent the entire state of the application. You can add other modules same as todo module.

Copy the following code in app.reducer.ts in app/state directory.


import { ActionReducerMap, MetaReducer } from '@ngrx/store';

import * as fromTodo from './todo/todo.reducer';

export interface AppState {
    todo: fromTodo.State;
}
export const appReducer: ActionReducerMap<AppState> = {
    todo: fromTodo.reducer
};

State Module

Now we’ll create the module for the ngrx state called StateModule, where we’ll define our reducers and effects.

Create a new file called state.module.ts in app/state directory and copy the following code into it.


import { CommonModule } from '@angular/common';
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store';

import { appReducer } from './app.reducer';
import { TodosEffects } from './todo/todo.effects';

@NgModule({
  imports: [
    CommonModule,
    StoreModule.forRoot(appReducer),
    EffectsModule.forRoot([TodosEffects])
  ],
  declarations: []
})

export class StateModule {

  static forRoot(): ModuleWithProviders {
    return {
      ngModule: StateModule
    };
  }

  constructor(@Optional() @SkipSelf() parentModule: StateModule) {
    if (parentModule) {
      throw new Error(
        'StateModule is already loaded. Import it in the AppModule only');
    }
  }
}

App Module

As of now, we’ve created StateModule and CoreModule, we need to import it in the main module called AppModule  

Now update your AppModule as following:    


import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule, RouteReuseStrategy, Routes } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { StateModule } from './state/state.module';
import { CoreModule } from './core/core.module';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { ComponentsModule } from './components/components.module';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [
    BrowserModule,
    IonicModule.forRoot(),
    AppRoutingModule,
    ComponentsModule,
    StateModule.forRoot(),
    CoreModule.forRoot()
  ],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

STEP 8: TODO LIST PAGE

Now, ngrx setup is completed and it’s time to update our ionic page to fetch a list of todos using ngrx.

Update todo-list.page.ts as following:


 import { Component, OnInit } from '@angular/core';
import { Observable } from "rxjs";

import { Todo } from "../../state/todo/todo.model";

import { Store } from "@ngrx/store";
import { AppState } from '../../state/app.reducer';

import * as fromStore from '../../state/app.reducer';
import * as fromTodo from '../../state/todo/todo.actions';
import { getAllTodos, getLoading, getError } from '../../state/todo';
@Component({
  selector: 'app-todo-list',
  templateUrl: './todo-list.page.html',
  styleUrls: ['./todo-list.page.scss'],
})
export class TodoListPage implements OnInit {
  todos$: Observable<Array<Todo>>
  loading$: Observable<boolean>;
  error$: Observable<string>;

  constructor(private store: Store<fromStore.AppState>) {
    this.todos$ = this.store.select(getAllTodos);
    this.loading$ = this.store.select(getLoading);
    this.error$ = this.store.select(getError);
  }

  ngOnInit() {
    this.store.dispatch(new fromTodo.GetAllTodos());
  }

}

In the above code you can see that we are fetching data from selectors:


 this.todos$ = this.store.select(getAllTodos);

and update todo-list.page.html as following:


<ion-header>
  <ion-toolbar>
      <ion-buttons slot="start">
          <ion-menu-button></ion-menu-button>
        </ion-buttons>
    <ion-title>Todo List</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding="">
  
    <ion-list>
        <ion-item *ngfor="let todo of (todos$|async)">
          {{todo.name}}
        </ion-item>
      </ion-list>
</ion-content>

Our todo app is ready now
.
Run the application using ionic serve. It will run the ionic app and open /todo-list url and it will display the following result.

How to use ngrx/store with Ionic 4

Here you can see that it has displayed two results Shopping and Meeting from in-memory-web-service.

Download full tutorial of todo app from github


Thanks for reading How to use ngrx/store with Ionic 4 article.

Also, read my other articles :