Monday, Dec 28, 2020
Async in MobX
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 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.