Sunday, Jul 28, 2019
Async Web API calls using React with Mobx
Introduction
React is a JavaScript library for building user interfaces. Mobx is simple stage management library that provides mechanism to store and update the application state. Its most powerfull feature is that it can make your variables observable. It will observe for changes, then tell React to re-render the page and show changes instantly. Mobx observes references, not the values itself. For example, if you have object:
{
name: "John",
address : {
city: "New York"
}
list: ["a", "b", "c"];
}
Mobx will observe properties: “name
, address
, city
, list
” and not the values: “John
, New York
, a
, b
, c
”.
We will be calling the server-side API asynchronously and then wait for results, making our web application more responsive. This frees up server’s CPU to perform other tasks while it waits for results. Instead of using promises and “.then
” function, we will be using “async
” and “await
”, which is the approach I like more.
Basic Setup
I will assume that you have already Node.js and npm installed on your system and you know how to setup React. The next thing you need to do is to install Mobx. Just type in your folder project “npm install mobx –save” and “npm install mobx-react –save” on your cmd and you ready to go.
Creating a Service and a Store
First we will be creating a service for our HTTP calls to the Web API, and it will contain get
, post
, put
and delete
methods. Let’s create a country service:
const webApiUrl = "http://country.local/api/Country";
class CountryService {
get = async (urlParams) => {
const options = {
method: "GET",
}
const request = new Request(webApiUrl + "?" + urlParams, options);
const response = await fetch(request);
return response.json();
}
post = async (model) => {
const headers = new Headers();
headers.append("Content-Type", "application/json");
var options = {
method: "POST",
headers,
body: JSON.stringify(model)
}
const request = new Request(webApiUrl, options);
const response = await fetch(request);
return response;
}
put = async (model) => {
const headers = new Headers()
headers.append("Content-Type", "application/json");
var options = {
method: "PUT",
headers,
body: JSON.stringify(model)
}
const request = new Request(webApiUrl, options);
const response = await fetch(request);
return response;
}
delete = async (id) => {
const headers = new Headers();
headers.append("Content-Type", "application/json");
const options = {
method: "DELETE",
headers
}
const request = new Request(webApiUrl + "/" + id, options);
const response = await fetch(request);
return response;
}
export default CountryService;
Next we will create a Store
and inject our service through Store’s constructor. Store is a place where you keep your reusable logic and application’s UI state that will be used by your components.
import { observable, runInAction, decorate } from 'mobx';
import CountryService from './CountryService'
class CountryStore {
constructor(){
this.countryService = new CountryService();
}
countryData = {
model: []
};
status = "initial";
searchQuery = "";
getCountriesAsync = async () => {
try {
var params = {
pageNumber: this.countryData.pageNumber,
searchQuery: this.searchQuery,
isAscending: this.countryData.isAscending
};
const urlParams = new URLSearchParams(Object.entries(params));
const data = await this.countryService.get(urlParams)
runInAction(() => {
this.countryData = data;
});
} catch (error) {
runInAction(() => {
this.status = "error";
});
}
};
createCountryAsync = async (model) => {
try {
const response = await this.countryService.post(model);
if (response.status === 201) {
runInAction(() => {
this.status = "success";
})
}
} catch (error) {
runInAction(() => {
this.status = "error";
});
}
};
updateCountryAsync = async (vehicle) => {
try {
const response = await this.countryService.put(vehicle)
if (response.status === 200) {
runInAction(() => {
this.status = "success";
})
}
} catch (error) {
runInAction(() => {
this.status = "error";
});
}
};
deleteCountryAsync = async (id) => {
try {
const response = await this.countryService.delete(id);
if (response.status === 204) {
runInAction(() => {
this.status = "success";
})
}
} catch (error) {
runInAction(() => {
this.status = "error";
});
}
}
}
decorate(CountryStore, {
countryData: observable,
searchQuery: observable,
status: observable
});
export default new CountryStore();
This is where Mobx
kicks in. We decorate our variables observable and the magic happens - Mobx
will track their state and apply changes instantly. For this magic to happen we also need to make an action to modify the state. I’ve chosen the run in action
approach, but there are few other options.
In this example I’m getting the countryData
from the server that contains paging and sorting parameters, and then I’m sending that same data back through urlParams
when it’s state changes (server side paging and sorting is used). Along with it I’m sending a searchQuery
from the UI. Create
, Update
and Delete
functions are simple, I’m passing a model
and id
.
Components
Next we will create components for our CRUD
methods. Components let you split the UI into independent, reusable pieces. In other words a component is a class
or function
that accepts inputs(props
);
import * as React from 'react';
import { observer, inject } from 'mobx-react';
class CountryList extends React.Component {
componentDidMount() {
this.props.CountryStore.getCountriesAsync();
}
searchCountry = (e) => {
if (e.key === "Enter") {
this.props.CountryStore.search = e.target.value;
}
}
sortCountry = () => {
this.props.CountryStore.countryData.isAscending = this.props.CountryStore.countryData.isAscending
? false : true
this.props.CountryStore.getCountriesAsync()
}
render() {
return (
<div>
<input type="search" placeholder="Search" onKeyPress={this.searchCountry} />
<input type="submit" value="Sort" onClick={this.sortCountry}/>
<table>
<thead>
<tr>
<th>Name</th>
<th>City</th>
<th>Date Created</th>
</tr>
</thead>
<tbody>
{this.props.CountryStore.countryData.model.map(country => (
<tr key={country.id}>
<td>{country.name}</td>
<td>{country.city}</td>
<td>{country.dateCreated}</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
}
export default inject("CountryStore")(observer(CountryList));
Here we are calling the get function in componentDidMount
. It’s one of React’s lifecycle methods and it’s invoked immediately after a component is mounted. This is a good place for calling your Web API. We are calling the CountryStore
methods and properties with this.props
. CountryStore
is injected after the class
scope and class
is decorated to be an observer. Observers make sure that any data that is used during the rendering of a component forces a re-rendering upon change.
Methods for post
and put
are similar. Note that you need to implement have OnChange
event if you gonna change existing text on your input fields.
class Create extends React.Component {
CreateCountry = (e) => {
e.preventDefault();
this.props.CountryStore.createCountryAsync({
name: this.refs.name.value,
city: this.refs.city.value,
});
this.refs.name.value = null;
this.refs.city.value = null;
};
render() {
return (
<div>
<div>
<form onSubmit={this.CreateCountry}>
<div className="form-group">
<input ref="name" id="name" type="text" placeholder="Name"/>
</div>
<div className="form-group">
<input ref="city" id="city" type="text" placeholder="City"/>
</div>
<button type="submit">Save</button>
</form>
</div>
</div>
)
}
}
export default inject("VehicleMakeStore")(observer(Create));
Delete method is very simple, you just pass id
of an entity to delete:
Delete = (e) => {
e.preventDefault();
this.props.CountryStore.deleteCountryAsync(this.props.id)
}
In this post we have learned how to implement basic CRUD operations using React and Mobx. Many people use Redux for state management on React, but for me Mobx is simpler and cleaner way to go with React.