Async in MobX
Development | Josip Tomaic

Async in MobX

Saturday, Sep 19, 2020 • 5 min read
An introduction to using async actions with MobX library in React applications, highlighting both good and bad practices.

Mobx is a library that has the main purpose to make state management as simple as possible and also to make state management scalable. A combination of React and MobX provides a really powerful toolset for creating user interfaces. React is used to render the UI by translating application state into tree of renderable components, while MobX provides a mechanism to manage and update rendered application state that React uses.

One very important feature of MobX is that it makes your variables observable. This means that MobX will observe and follow these variables throughout the application flow and detect when these variables change value. This value change will trigger React re-rendering of the current page (entire page or a specific part of the page) and changes will be shown immediately. Here is my take on basic concepts in working with async actions with MobX.

How MobX works?

Mobx state management

MobX state management can be divided into four segments. These are:

  • Actions
  • State
  • Computed values
  • Reactions

Actions are methods (functions) triggered by events that should only be used for situations where there is need for updating application state and not for anything else. Methods that are only lookups, filters and similar should not be marked as actions.

State is represented as a minimalist defined observable that should not contain redundant or derivable data. It can contain classes, arrays etc.

Computed values are values that are composed of state data. These values derive value from observables and are updated when one of the observables used in computed value has changed. These updates happen automatically and only when required.

Reactions are methods that, as computed values, react to state changes, but they produce “side-effects” and not the value itself. Side-effect can be, for example, an update of the UI.

Basic application setup

To be able to run these kind of applications you need to have installed Node.js and npm.

To install MobX using npm use these commands in cmd:
npm install mobx –save
npm install mobx-react –save

To start an application just use this command in cmd:
npm start

Furthermore, in this blog post, you will see a minimalist example of async functionality in MobX using a really simple application in which dummy data is fetched from the random API found on the web.
After the data is fetched, the observable variable value will be set to it. Multiple approaches are explained in the next section.

Creating a service

import { flow } from 'mobx'

const apiUrl = 'http://dummy.restapiexample.com/api/v1/employees'

class EmployeeService {
    getEmployees = () => {
        return fetch(apiUrl).then((response) => response.json())
    }

    getEmployeesAsyncAwait = async () => {
        const response = await fetch(apiUrl)
        const data = await response.json()
        return data
    }

    getEmployeesGenerator = flow(function* () {
        const response = yield fetch(apiUrl)
        const data = yield response.json()
        return data
    })
}

export default EmployeeService

Here you can see the service that is used in this minimalist example. This Employee service contains three methods that do the same thing - fetching employees from dummy url stored in the apiUrl variable.

getEmployees method fetches the employees and returns a Promise. After calling this method, returned promise needs to be resolved using .then.

getEmployeesAsyncAwait method fetches the employees using async/await. This means that application flow will wait for the response from dummy API and then return fetched data which is in this case JSON. In my opinion, this is the cleanest way of fetching data compared to other ways mentioned in this example.

getEmployeesGenerator method shows the usage of flow keyword and generators. When using flow, every await is replaced with yield keyword. The function represents the generator function and * sign is important because it denotes the function and yield gives control to the iterator.

Creating a store

import { observable, action, runInAction, configure, decorate } from 'mobx'
import { EmployeeService } from '../services'
configure({ enforceActions: 'observed' })

class EmployeeStore {
    employeesList = {}

    constructor() {
        this.employeeService = new EmployeeService()
    }

    setEmployeesList = (apiData) => {
        this.employeesList = apiData
    }

    //bad way of updating data
    getEmployeesBadWay = () => {
        this.employeeService.getEmployees().then((data) => {
            this.employeesList = data
        })
    }

    //correct way of updating data
    getEmployeesCorrectWay = () => {
        this.employeeService.getEmployees().then((data) => {
            runInAction(() => {
                this.setEmployeesList(data)
            })
        })
    }

    //correct way inline
    getEmployeesCorrectWayInline = () => {
        this.employeeService.getEmployees().then((data) => {
            runInAction(() => {
                this.employeesList = data
            })
        })
    }

    //correct way with async await (the cleanest way in my opinion)
    getEmployeesCorrectWayAsync = async () => {
        const data = await this.employeeService.getEmployeesAsyncAwait()

        runInAction(() => {
            this.employeesList = data
        })
    }

    //using flow and yield to fetch data
    getEmployeesGenerator = async () => {
        const data = await this.employeeService.getEmployeesGenerator()
        runInAction(() => {
            this.employeesList = data
        })
    }
}

decorate(EmployeeStore, {
    employeesList: observable,
    setEmployeesList: action,
})

export default new EmployeeStore()

Here you can see the whole store implementation which will be explained in next sections.

The bad way of updating state

//bad way of updating data
getEmployeesBadWay = () => {
    this.employeeService.getEmployees().then((data) => {
        this.employeesList = data
    })
}

This part of code does its job fine and it works as it should but there is a little problem with this code. Yes, the data is fetched from the API and employeesList value is updated with fetched data but this part of the code is not written correctly because it is not written in a MobX way as it should be.

You will see the problem in the next section.

Making our application unusable - Why?

So, to continue explanation from the previous section, we need to add a specific line of the code in our app.

configure({ enforceActions: 'observed' })

What does this line of code do? It forces the developer to write every change of any observable inside action, according to MobX rules.

So, you have added this line of code in your app and you try to call the first method from the service and MobX will throw you an error which says:

Unhandled Rejection (Error): [mobx] Since strict-mode is enabled, changing observed observable values outside actions is not allowed. Please wrap the code in an 'action' if this change is intended.

In order to fix this error you need to wrap your observable change within an action method. You can write an action method in which observable value will be updated or you can do it inline using runInAction from MobX library.

Next sections will illustrate how to use different approaches for updating state.

Updating state using runInAction (inline action)

//correct way inline
getEmployeesCorrectWayInline = () => {
    this.employeeService.getEmployees().then((data) => {
        runInAction(() => {
            this.employeesList = data
        })
    })
}

This image shows the usage of runInAction functionality from MobX. What runInAction really does is it takes the block of code and runs it in anonymous action. This is useful because you can create actions on the fly and not have action method for every observable change.
So runInAction is just another form of writing action(f)().

Updating state using action

setEmployeesList = (apiData) => {
    this.employeesList = apiData
}

//correct way of updating data
getEmployeesCorrectWay = () => {
    this.employeeService.getEmployees().then((data) => {
        runInAction(() => {
            this.setEmployeesList(data)
        })
    })
}

In this section you can see the usage of action method setEmployeesList to perform the observable value change.

It is important to mention that type of methods and variables is defined in decorate part of a code.

decorate(EmployeeStore, {
    employeesList: observable,
    setEmployeesList: action,
})

Updating state using runInAction and async await

//correct way with async await (the cleanest way in my opinion)
getEmployeesCorrectWayAsync = async () => {
    const data = await this.employeeService.getEmployeesAsyncAwait()

    runInAction(() => {
        this.employeesList = data
    })
}

As you can see here the data is fetched using async await and then observable value is changed on the fly using runInAction.

Updating state using flow and yield keywords

getEmployeesGenerator = flow(function* () {
    const response = yield fetch(apiUrl)
    const data = yield response.json()
    return data
})

In this example a service method is shown because of its similarity to the previous example. As I mentioned earlier, every await is replaced by yield as explained in the service section of this post.

For further details about this functionality of MobX and MobX in general, please visit MobX official documentation.