How To Manage State In Ionic 2+ Apps With MobX - Part 1
A year ago I wrote a tutorial for managing application state with ngrx (which is based on Redux). Since then I've been hearing a lot about MobX and how it's much easier to use than Redux.
So in this tutorial, I decided to build a similar Ionic 2+ app as in the ngrx tutorial, but this time using MobX instead, to see how it compares to Redux.
This tutorial is for Ionic 2+ apps, up-to-date with Ionic 3.5.2.
This tutorial is part of a 2-part series:
Part 1 - Using @observable and @computed (this post)
Part 2 - Using autorun and reaction
The Ionic app we're creating in this tutorial is a very simple birthday tracker. You can add/update/delete birthdays and it will also show today's birthdays.
###What is MobX? MobX is a library that helps you manage state in applications in a simple way. One of its big advantages is that it doesn't require as much boilerplate code as Redux does.
Both MobX and Redux were developed by members of the React community for managing state in React apps but both can be used in apps built with other frameworks like Angular.
To understand MobX, it's good to keep its philosophy in mind while you're going through this tutorial:
Anything that can be derived from the application state, should be derived. Automatically.
###Store A good practice when using MobX is to move any logic that manages state out of the components and into a store. For example, in our app, the store will contain an array of birthdays.
You don't need to follow this practice though, it's entirely up to you where you want to put this logic. MobX will work either way.
In order for MobX to detect changes in the state, we need to tell it which properties to track. We can do that by using the following decorators: @observable
and @computed
.
####@observable
@observable
should be used for properties that can be changed. For example, in this app, we'll have an array of birthday objects in the store and we can add/update/delete items in the array.
By decorating the array with @observable
, MobX will automatically detect when a change happens in the array and let the components know that they can update their views. Because of this, we can set the ChangeDetectionStrategy
to OnPush
, which will improve the performance of the app.
For a better understanding of change detection read this.
####@computed
@computed
is used for properties that can be derived from @observable
properties.
For example, the list of today's birthdays can be derived from the list of all birthdays. We can write a getter method for the birthdaysToday
property which will contain the logic needed to filter the list of all birthdays.
MobX will detect which @observable
properties are used within the getter and when these are changed it will update the computed value.
####TL;DR To summarize all of the above, you just need to add some decorators to your code and your application will magically update itself whenever the state changes.
Don't believe me? Let's start building our app and you'll see it's really that easy! :)
###Create App We'll start by creating our Ionic 3 app
$ ionic start ionic3-tutorial-mobx blank
$ cd ionic3-tutorial-mobx
###Install dependencies
We need to install mobx
and mobx-angular
.
$ npm install mobx mobx-angular --save
Another dependency we'll be using is angular-uuid
to generate unique ids.
$ npm install angular2-uuid --save
###Define Birthday Let's first define what a Birthday object is.
// location: src/models/birthday.ts
import { observable, computed } from "mobx-angular";
export class Birthday {
id: string;
@observable name: string;
@observable date: string;
constructor() {}
@computed get parsedDate(): Date {
return new Date(this.date);
}
}
As you can see I've decorated name
and date
with @observable
. These values can be changed by the user and because of the @observable
decorator, MobX knows that it needs to track changes on these properties.
date
will contain the birthday date in ISO format, but we'll need it as an actual Date
object later on, so I've created a @computed
property for it.
As I mentioned before, this means that every time the date
value changes, MobX will automatically update the value of parsedDate
.
###Define BirthdayStore
BirthdayStore
will contain the birthdays
array and methods to add/update/delete items in this array.
// location: src/stores/birthday.store.ts
import { observable, action, computed } from "mobx-angular";
import { Injectable } from "@angular/core";
import { Birthday } from "../models/birthday";
import { UUID } from "angular2-uuid";
@Injectable()
export class BirthdayStore {
@observable birthdays: Birthday[] = [];
@action addBirthday(birthday: Birthday) {
birthday.id = UUID.UUID();
this.birthdays.push(birthday);
}
@action deleteBirthday(birthday: Birthday) {
let index = this.birthdays.findIndex(b => b.id == birthday.id);
this.birthdays.splice(index, 1);
}
@action updateBirthday(birthday: Birthday) {
let index = this.birthdays.findIndex(b => b.id == birthday.id);
this.birthdays[index] = birthday;
}
}
You probably noticed that we are using another MobX decorator: @action
. You don't need to use this decorator, MobX will still work without it, but it's a good practice to use this decorator to explicitly define where mutations to observable properties happen.
If you want to enforce the use of
@action
, you can tell MobX to use strict mode.
Our app also needs a list of today's birthdays. Since this can be derived from the birthdays
array, we'll make it a @computed
property and add it to BirthdayStore
.
@computed get birthdaysToday() {
let today = new Date();
return this.birthdays
.filter(b => b.parsedDate.getMonth() == today.getMonth() &&
b.parsedDate.getDate() == today.getDate())
.map(b => ({
name: b.name,
age: today.getFullYear() - b.parsedDate.getFullYear()
}));
}
MobX will know that the getter uses the birthdays
array, so whenever a change happens to that array, it will re-run the getter for birthdaysToday
.
###Let's build the UI We'll create 2 pages for our app, one to display the list of birthdays (HomePage) and one to add/edit/delete a birthday (DetailsPage).
###HomePage
We'll implement the template home.html first, which will display 2 lists: today's birthdays and all birthdays from the BirthdayStore
.
In order for MobX to automatically update the page, we need to use the *mobxAutorun
directive (which is provided by mobx-angular).
<ion-header>
<ion-navbar>
<ion-title>
🎂 Birthdays 🎉
</ion-title>
<ion-buttons end>
<button ion-button (click)="showDetail()">
<ion-icon name="add"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content class="home" *mobxAutorun>
<ion-item-group>
<ion-item-divider color="light">{{ today | date:'fullDate' }}</ion-item-divider>
<ion-item *ngIf="store.birthdaysToday.length == 0">
<div>No cake 🎂 today...</div>
</ion-item>
<ion-item *ngFor="let birthday of store.birthdaysToday">
{{ birthday.name }} is {{ birthday.age}} years old today 🎂
</ion-item>
</ion-item-group>
<ion-item-group>
<ion-item-divider color="light">All Birthdays</ion-item-divider>
<button ion-item *ngFor="let birthday of store.birthdays" (click)="showDetail(birthday)">
<div>{{ birthday.name }}</div>
<div item-end>{{ birthday.date | date:'yMMMMd' }}</div>
</button>
</ion-item-group>
</ion-content>
In home.ts we need to inject BirthdayStore
to be able to use it in the template.
As mentioned earlier, we can now set the change detection strategy to OnPush
, which will improve the performance of our app.
import { Component, ChangeDetectionStrategy } from "@angular/core";
import { ModalController, NavController } from "ionic-angular";
import { Birthday } from "../../models/birthday";
import { BirthdayStore } from "../../stores/birthday.store";
import { DetailsPage } from "../details/details";
@Component({
selector: "page-home",
templateUrl: "home.html",
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HomePage {
public today = new Date();
constructor(
public nav: NavController,
public modalCtrl: ModalController,
public store: BirthdayStore
) {}
showDetail(birthday: Birthday) {
let modal = this.modalCtrl.create(DetailsPage, { birthday: birthday });
modal.present();
}
}
###DetailsPage Add a new page with this command:
$ ionic g page details
Add the following code for the template in details.html.
<ion-header>
<ion-navbar>
<ion-title>{{ action }} Birthday</ion-title>
<ion-buttons end *ngIf="!isNew">
<button ion-button (click)="delete()">
<ion-icon name="trash"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content padding class="details">
<ion-list inset>
<ion-item>
<ion-label>Name</ion-label>
<ion-input text-right type="text" [(ngModel)]="birthday.name"></ion-input>
</ion-item>
<ion-item>
<ion-label>Birthday</ion-label>
<ion-datetime displayFormat="MMMM D, YYYY" pickerFormat="MMMM D YYYY" [(ngModel)]="birthday.date"></ion-datetime>
</ion-item>
</ion-list>
<button ion-button block (click)="save()">Save</button>
</ion-content>
Add the following code to details.ts
. We're injecting the BirthdayStore
again here so we can call the add/update/delete methods.
import { Component } from "@angular/core";
import { NavController, NavParams, ViewController } from "ionic-angular";
import { Birthday } from "../../models/birthday";
import { BirthdayStore } from "../../stores/birthday.store";
@Component({
selector: "page-details",
templateUrl: "details.html"
})
export class DetailsPage {
public birthday: Birthday = new Birthday();
public isNew: boolean;
public action: string;
constructor(
public navCtrl: NavController,
public viewCtrl: ViewController,
public navParams: NavParams,
public store: BirthdayStore
) {}
ionViewWillEnter() {
let selectedBirthday = this.navParams.get("birthday");
if (selectedBirthday) {
this.birthday = Object.assign(new Birthday(), selectedBirthday);
this.isNew = false;
this.action = "Edit";
} else {
this.isNew = true;
this.action = "Add";
}
}
save() {
if (this.isNew) {
this.store.addBirthday(this.birthday);
} else {
this.store.updateBirthday(this.birthday);
}
this.dismiss();
}
delete() {
this.store.deleteBirthday(this.birthday);
this.dismiss();
}
dismiss() {
this.viewCtrl.dismiss(this.birthday);
}
}
###Configure all dependencies
Don't forget to modify app.module.ts to import MobxAngularModule
and add BirthdayStore
to the providers.
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 { DetailsPage } from "./../pages/details/details";
import { MobxAngularModule } from "mobx-angular";
import { BirthdayStore } from "../stores/birthday.store";
@NgModule({
declarations: [MyApp, HomePage, DetailsPage],
imports: [BrowserModule, IonicModule.forRoot(MyApp), MobxAngularModule],
bootstrap: [IonicApp],
entryComponents: [MyApp, HomePage, DetailsPage],
providers: [
StatusBar,
SplashScreen,
{ provide: ErrorHandler, useClass: IonicErrorHandler },
BirthdayStore
]
})
export class AppModule {}
###We're Done! You can now test the app in the browser.
$ ionic serve
###MobX vs Redux When you compare the code in this tutorial with the code in the ngrx (Redux) tutorial, you see that the code for using MobX is much simpler.
All I really did was add some decorators and a directive to the code and that's it. Magic! :) You don't really have to learn a lot of new things, you can still write your code the way you want to.
I mentioned a couple of times that MobX doesn't really care how you structure your code, whereas Redux has very clearly defined rules, with its actions, reducers, etc.
I actually don't mind that about Redux, even if it means that you end up with all that boilerplate code. Once you understand the Redux pattern, you always know where what happens. So if other developers need to work on your app and they know Redux, they will have no trouble to understand the code.
With MobX the code can be scattered all over the app and it will still work, but will probably not be easy to maintain. It's entirely up to the developer(s) of the app to decide how they structure the code.
Another big difference between MobX and Redux is that MobX uses mutable objects, whereas Redux is all about immutability.
I think that for complex apps, it's a good idea to consider using Redux instead of MobX, but it really depends on your own preference which one you would use in your app.
If you need help deciding which one to choose, watch this talk by Preethi Kasireddy on React Conf 2017: MobX vs Redux: Comparing the Opposing Paradigms.
What's Next?
Now we have an app that can perform add/update/delete operations on our data, but the next time we start the app, all the data is gone. That kind of defeats the purpose of having a birthday tracker, so in the next part we'll have a look at how to use MobX to persist the data.