How to use ngrx/store in Ionic Framework 3

 

It’s not that easy to wrap your head around the whole state management of SPA’s. There’s not that much on the web about it & its such a needed concept when your application grows. If you have a small app that doesn’t take advantage of needing components to talk to each other, then it’s not the time to start with ngrx/store. In my applications that I build, when I need to rely on events in Ionic I think it’s time to look at state management.

Built with ionic 3.6.0

The setup

ionic start bigbeartechNgrx blank
cd bigbeartechNgrx
npm i --save @ngrx/store
ionic serve --lab

When you run all that then you should now have a blank Ionic Framework.

You should now see this in Ionic
This is what you should see after starting ionic serve

We need to create an action… It’s what you call when you want something to happen or change the state in some way.

Create ./src/actions/mealActions.ts

import { Action } from "@ngrx/store";
import { Meal } from "../reducers/mealReducer";

export const ADD_MEAL = "ADD_MEAL";
export const DELETE_MEAL = "DELETE_MEAL";
export const RESET = "RESET";

export class AddMeal implements Action {
  readonly type = ADD_MEAL;
  constructor(public payload: Meal) {}
}

export class DeleteMeal implements Action {
  readonly type = DELETE_MEAL;

  constructor(public payload: any) {}
}

export class ResetMeal implements Action {
  readonly type = RESET;

  constructor(public payload: any) {}
}

export type All = AddMeal | DeleteMeal | ResetMeal;

Now you’re going to need something to change the state with…..These are called reducers!  They get the payload from the actions, then return the state.

Create ./src/reducers/mealReducer.ts

import { ActionReducer, Action } from "@ngrx/store";

import * as MealActions from "../actions/mealActions";

export type Action = MealActions.All;

export interface Meal {
  id: string;
  title: string;
  content: string;
}

export interface AppState {
  meals: [Meal];
}

export function mealReducer(state = [], action) {
  console.log(action);
  switch (action.type) {
    case MealActions.ADD_MEAL:
      return [...state, ...action.payload];

    case MealActions.DELETE_MEAL:
      return state.filter(meal => meal.id !== action.payload.id);

    case MealActions.RESET:
      return [];

    default:
      return state;
  }
}

It’s looking good, but we’re missing one thing. How is angular going to know anything about what a reducer is? Ahhhhh….we need to register it in the app.module.ts file in imports.

Go to ./src/app/app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { ErrorHandler, NgModule } from '@angular/core';
import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
import { SplashScreen } from '@ionic-native/splash-screen';
import { StatusBar } from '@ionic-native/status-bar';

import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';

import { StoreModule } from "@ngrx/store";

@NgModule({
  declarations: [MyApp, HomePage],
  imports: [
    BrowserModule,
    IonicModule.forRoot(MyApp),
    StoreModule.forRoot({ meals: mealReducer })
  ],
  bootstrap: [IonicApp],
  entryComponents: [MyApp, HomePage],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: ErrorHandler, useClass: IonicErrorHandler }
  ]
})
export class AppModule {}

But wait a min we can clean this up a bit can’t we? How about we extract the call to reducer & create ./src/reducers/reducers.ts

import { mealReducer } from "./mealReducer";
export const ROOT_REDUCER = {
    meals: mealReducer
};

You don’t have to create the file if you don’t want to. But I always like to clean things up a bit & the app.module.ts can get big in some projects if you’re not using lazy loading.

Edit ./src/app/app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { ErrorHandler, NgModule } from '@angular/core';
import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
import { SplashScreen } from '@ionic-native/splash-screen';
import { StatusBar } from '@ionic-native/status-bar';

import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';

import { StoreModule } from "@ngrx/store";
import { ROOT_REDUCER } from './../reducers/reducers';

@NgModule({
  declarations: [MyApp, HomePage],
  imports: [
    BrowserModule,
    IonicModule.forRoot(MyApp),
    StoreModule.forRoot(ROOT_REDUCER)
  ],
  bootstrap: [IonicApp],
  entryComponents: [MyApp, HomePage],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: ErrorHandler, useClass: IonicErrorHandler }
  ]
})
export class AppModule {}

Ok there is a lot of boilerplate code here, but it’s all needed in the long run.

Edit ./src/pages/home-page.ts

import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';

import { Store } from "@ngrx/store";
import * as MealActions from "./../../actions/mealActions";
import { AppState } from './../../reducers/mealReducer';
import { Observable } from "rxjs/Observable";

@Component({
  selector: "page-home",
  templateUrl: "home.html"
})
export class HomePage {
  form: any = {
    title: "",
    content: ""
  };
  meals: Observable<any>;

  constructor(public navCtrl: NavController, private store: Store<AppState>) {
    this.meals = store.select<any>("meals");
  }

  addMeal() {
    let id = Math.random().toString(36).substr(2, 10);
    this.store.dispatch(
      new MealActions.AddMeal({
        id: id,
        title: this.form.title,
        content: this.form.content
      })
    );
  }

  removeMeal(_meal) {
    this.store.dispatch(new MealActions.DeleteMeal({ id: _meal.id }));
  }

  resetMeals() {
    this.store.dispatch(new MealActions.ResetMeal({}));
  }
}

We use the dispatch on the store with the action that we created called mealActions.ts ….. The action will then send the payload to mealreducer, which will take the payload and return it back to the state. Best of all…it’s reactive! That means that it’s using RxJS under the hood to subscribe & send the data. So, no more subscribing to ionic events & updating arrays.

We have the backend about done, but what about the Ui?  It’s coming, don’t think too far in advance, absorb this information because it’s a lot to understand at first, but it’s easy in the end.

Ok lets get down to the nitty gritty with what the user sees!

Ionic Framework UI setup

Edit ./src/pages/home-page.html

<ion-header>
  <ion-navbar>
    <ion-title>
      Meals
    </ion-title>
    <ion-buttons end>
      <button ion-button (tap)="addMeal()">Add</button>
      <button ion-button (tap)="resetMeals()">Reset</button>
    </ion-buttons>
  </ion-navbar>
</ion-header>

<ion-content>
  <ion-list>
    <ion-item>
      <ion-label>Title</ion-label>
      <ion-input [(ngModel)]="form.title" placeholder="Meal Title"></ion-input>
    </ion-item>
    <ion-item>
      <ion-label>Content</ion-label>
      <ion-textarea [(ngModel)]="form.content" placeholder="Meal Content"></ion-textarea>
    </ion-item>
    <ion-list-header>Meals</ion-list-header>
    <button ion-item *ngFor="let meal of meals | async" (tap)="removeMeal(meal)">
      <h2>{{meal.title}}</h2>
      <p>{{meal.content}}</p>
    </button>
  </ion-list>
  

</ion-content>

The form is just using [(ngModel)]….which if this was a production app, you would probably be using the formBuilder. What about the | async on the end? It’s just telling angular that the array is actually an Observable so this data can change.

The user fills out the form data, then they click the Add button, it then sends the click to addMeal function, which the addMeal function will send an action to the reducer. The reducer returns the state, then sends the data to store.select, which we tell it to get the meals. Async now updates the list with a new meal.

This is the way I understand it & see it happening in my mind.  It might not be exactly the way it works.  I’m sure I’m leaving out a lot of things it’s doing in the background. You don’t need to worry about how it works at first if you don’t want to.

Just know that actions > reducers – return state > Observables update view.

Hope this helps people & if you did learn a lot please share the post so it helps others. @ngrx/store seems to have went through a lot of revisions, so all the blog posts out there where already out of date.

Historical Comment Archive