How To Manage State In Ionic 2+ Apps With MobX - Part 2

In the first part of this tutorial we've built an app that can add/update/delete birthdays in an array.

Now it's time to write the code to persist the array to a database. In the ngrx tutorial I was using PouchDB, but for this one I'll keep it simple and use Ionic Storage so we can focus on MobX.

If you want to use PouchDB instead, you can have a look at my PouchDB tutorial.

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
Part 2 - Using autorun and reaction (this post)

What are our options?

Before we start writing code, let's have a look at what our options are for saving the data.

As mentioned earlier, we will be using Ionic Storage to save the entire birthdays array as a JSON string.

We can do that by adding code to the add/update/delete methods in BirthdayStore. I don't like this strategy because we will be writing duplicate code.

Since we are already using MobX, we can detect changes to the array and automatically save data whenever a change happens.

Let's have a look at the MobX functions we can use for reacting to data changes:

  • autorun: you can pass a function to autorun which will be executed whenever the observable objects used in the function have changed.
  • reaction: similar to autorun, but takes in an extra function that defines exactly which observable data to react to.

Let's have a look a the code to understand these 2 functions better.

Create service

First, we'll create a service to handle the calls to Ionic Storage for us.

This service has a method to get data from the database and deserialize it into Birthday objects. And it will also have a method to save the data as JSON.

import { Injectable } from '@angular/core';  
import { Storage } from '@ionic/storage';  
import { Birthday } from '../models/birthday';

@Injectable()
export class BirthdayService {

    private STORAGE_KEY = 'BIRTHDAYS';

    constructor (private storage: Storage) { }

    saveAll(birthdays: Birthday[]): Promise<any> {
        return this.storage.set(this.STORAGE_KEY, JSON.stringify(birthdays));
    }

    getAll(): Promise<Birthday[]> {
        return this.storage.ready()
            .then(() => this.storage.get(this.STORAGE_KEY))
            .then(data => {
                const birthdays = JSON.parse(data);

                if (birthdays) {
                    return birthdays.map(b => Object.assign(new Birthday(), b));
                }

                return [];
            })
    }
} 

If you have more complex objects have a look at using serializr to have more control over how the data is serialized to JSON.

Don't forget to import this service in app.module.ts and add it to the providers array.

We also need to import IonicStorageModule.

// ... existing imports here

import { BirthdayService } from './../services/birthday.service';  
import { IonicStorageModule } from "@ionic/storage";

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

Update BirthdayStore

Add the following imports to BirthdayStore.

import { BirthdayService } from './../services/birthday.service';  
import * as mobx from 'mobx';  

Next, we'll update the constructor to inject BirthdayService and we'll add 2 methods to get and save the data.

constructor(private storage: BirthdayService) { }

private getData() {  
    return this.storage
            .getAll()
            .then(data => this.birthdays = data);
}

private saveData() {  
    console.log('Saving data...');

    this.storage.saveAll(this.birthdays)
        .catch(() => {
            console.error('Uh oh... something went wrong, reloading data...');
            this.getData();
        })
}

The saveData method will be called after the array is changed, so if it fails to save the data, we'll reload data from the database, so it's kept in sync.

Option 1: Saving data with autorun

Let's modify the constructor and get the data for the birthdays array.

After the data is loaded we'll call mobx.autorun to automatically save whenever a change happens in the array.

constructor(private storage: BirthdayService) {  
    this.getData()
        .then(() => mobx.autorun(() => this.saveData()));
}

If you run the app now with ionic serve and check the Console output you'll see that saveData is called directly on load of the app.

After that, it's only called when you do modifications and add birthdays.

This is normal behavior for the autorun function because it needs that first run to determine which data changes it should react to.

In our case, we're calling JSON.stringify(birthdays) on the observable birthdays array so autorun will detect that on the first run and it will invoke the saveData function every time a change happens in the array.

If you don't want this behavior, you can use reaction instead.

Option 2: Saving data with reaction

The reaction function takes in 2 functions as parameters.

In the first function, you need to define which data you want to observe.

The second function takes the output of the first function as input and calls the code for saving the data.

So now we can change the constructor to this:

constructor(private storage: BirthdayService) {  
    this.getData()
        .then(() => mobx.reaction(
            () => this.birthdays.slice(),
            birthdays => this.saveData()
        ));
}

The first function is returning a shallow copy (using slice()) of the birthdays array. At first, I thought I could just return the birthdays array there, but if you do that no changes will be detected.

You need to specifically access the array to get MobX to understand that it needs to react to changes on it. I'm doing that by using slice() as is recommended here.

For a more detailed explanation on this, read this section in the MobX docs: What does MobX react to?

When you load the app again, you'll see that saveData is now only called when you add/update/delete a birthday.

Where to go from here

I've only covered the basics of MobX in this tutorial, there is more you can do with it. You can find the documentation here: mobx.js.org.

You should also have a look at the following repos on Github:

Michel Weststrate (creator of MobX) also has a couple of talks from conferences on YouTube. Here are a few I recommend to watch:

I hope this tutorial helped you get started with using MobX in your own apps. If you have any questions, leave them in the comments below and I'll do my best to answer them.