A Simple Guide to Mobx — Practical Examples
In this post, we will look into the core concepts of mobx with the ready examples.
Introduction — Mobx is a simple, state management and well tested library to make the components reactive. This is surprisingly fast and consistent. With Mobx, can create multiple stores in your project.
Core Concepts of Mobx:
- Observable States
- Computed Values based on observable states
- Reactions
Let’s Start . . .
Observable :
The observable() function automatically converts an object, an array, or a map into an observable entity.
- Default Usage: let list = observable({ item = 0 })
- Object: let list = observable.object({ item = 0 })
- Array: let list =observable.array([ ])
- Map: let list =observable.map(value);
- Primitives, functions and class- instances:
let item=observable.box(value)
Observable.ref: It will monitor the reference changes to the observables entity (object, array, map).
@observable.ref isUserAuthorized= null;
--------------------------------------------------------------------class Modal {
@observable.ref user = {
name: "Tom",
age: 30
};
constructor() {
autorun(() => console.log(this.user, "autorun"));
}
@action
updateUser({ name, age }) {
this.user = { name, age };
}
}
const store = new Modal();
setTimeout(() => store.updateUser({ name: "Tom", age: 30 }), 500);
setTimeout(() => store.updateUser({ name: "Tom", age: 30 }), 1000);
setTimeout(() => store.updateUser({ name: "Morjim", age: 28 }), 1500);
setTimeout(() => store.updateUser({ name: "Tom", age: 30 }), 2000);
********************************************************************Prints: Autorun for every change in observable object even if the values are same.// {name: "Tom", age: 30} "autorun"
// {name: "Tom", age: 30} "autorun"
// {name: "Tom", age: 30} "autorun"
// {name: "Morjim", age: 28} "autorun"
// {name: "Tom", age: 30} "autorun"
Observable.struct: It will do a structural check that means the properties of your object are compared and then decide if there is a change.
@observable.struct user = {name: "Tom", age: 30};
--------------------------------------------------------------------class Modal {
@observable.struct user = {
name: "Tom",
age: 30
};
constructor() {
autorun(() => console.log(this.user, "autorun"));
}
@action
updateUser({ name, age }) {
this.user = { name, age };
}
}
const store = new Modal();
setTimeout(() => store.updateUser({ name: "Tom", age: 30 }), 500);
setTimeout(() => store.updateUser({ name: "Tom", age: 30 }), 1000);
setTimeout(() => store.updateUser({ name: "Morjim", age: 28 }), 1500);
setTimeout(() => store.updateUser({ name: "Tom", age: 30 }), 2000);
********************************************************************Prints: Only change happen when previous values not matching as whole with the new values. In short, for a unique change.// {name: "Tom", age: 30} "autorun"
// {name: "Morjim", age: 28} "autorun"
// {name: "Tom", age: 30} "autorun"
Deep observability: MobX will apply deep observability to the observable instance as default. there will be cases where you may not want to monitor the state change beyond the first level. This we can achieved this by passing in { deep: false } as an option.
observable.object(value, decorators, { deep: false });
observable.map(values, { deep: false });
observable.array(values, { deep: false });
Observable.shallow: This is a similar to setting the { deep: false } option on the observable.
@observable.shallow items = {};
--------------------------------------------------------------------class Modal {
@observable.shallow user = {
shortName: "Talt",
age: 30,
fullName: {
firstName: "",
lastName: ""
}
}; constructor() {
autorun(() => console.log(Object.values(this.user), "autorun"));
reaction(
() => Object.values(this.user),
data => console.log(data, "reaction")
);
} @action
editShortName() {
this.user.shortName = "Molfer";
} @action
updateFullName(firstName, lastName) {
this.user.fullName = { firstName, lastName };
}@action
editFullName() {
this.user.fullName.firstName = "Molly";
this.user.fullName.lastName = "Ferguson";
}
}
const store = new Modal();
setTimeout(() => store.editFullName(), 1000);
setTimeout(() => store.editShortName(), 2000);
********************************************************************Prints:
// ["Talt", 30, {firstName: "", lastName: ""}] "autorun"
// ["Molfer",30, {firstName: "Molly",lastName: "Ferguson"}] "reaction"
// ["Molfer",30, {firstName: "Molly",lastName: "Ferguson"}] "autorun"
// NO REACTION ON editing FULL NAME@observable.shallow items = [];
-------------------------------------------------------------------class Modal {
@observable.shallow cart = []; constructor() {
autorun(() => console.log(this.cart.toJS(), "autorun"));
reaction(() => this.cart, data => console.log(data,"reaction"));
reaction(() => this.cart.length,
data => console.log(data, "reaction on length"));
}
@action
addItem(data) {
this.cart.push(data);
}
}
const store = new Modal();
setTimeout(() => store.addItem("10"), 1000);
setTimeout(() => store.addItem({ Name: "Malice" }), 2000);Prints:
// [] "autorun"
// 1 "reaction on length"
// ["10"] "autorun"
// 2 "reaction on length"
// ["10", Object] "autorun"
Observable.shallowBox: This is to create the Primitives, functions and class- instances into a shallow observable entity .
const box = observable.shallowBox(value) or
observable.box(value, { deep: false })
Computed:
It derive their value from other observables. If any of these depending observables change, the computed property changes as well.
class FrontLine {
@observable items = 10;
@observable amount = 150; constructor(items) {
this.items = items;
} @computed get total() {
return this.items * this.amount;
}
}const storeFront = new FrontLine(30);
console.log(storeFront.total);
********************************************************************Prints: In computed both items and amount are dependent observables.
// 4500
Computed.struct: This is to ensure only a real change in the object structure is considered for notification.
class Model {
@observable x = 10;
@observable y = 150;
constructor(data) {
this.x = data;
autorun(() => console.log(this.add, "autorun"));
reaction(
() => this.add,
() => console.log(this.add, `reaction on ${this.x} &
${this.y}`));
} @computed.struct get add() {
return {
val: this.x + this.y || 0
};
} @action
updateData({ x, y }) {
this.x = x;
this.y = y;
}
}
const store = new Model();setTimeout(() => store.updateData({ x: 200, y: 100 }), 1000);
setTimeout(() => store.updateData({ x: 100, y: 200 }), 2000);
setTimeout(() => store.updateData({ x: 101, y: 200 }), 2000);********************************************************************Prints:
// {val: 0} "autorun"
// {val: 300}"reaction on 200 & 100"
// {val: 300} "autorun"
// {val: 301} "reaction on 101 & 200"
// {val: 301} "autorun"
Action:
This ensures that all notifications for state changes are fired, but only after the completion of the action function.
action & action.bound (pre-binds the instance of the class to the method)
class githubReposList {
repos = observable.array([]);
showError = observable.box(false, { deep: false }); constructor() {
reaction(
() => this.repos.length,
() => console.log(`You have ${toJS(this.repos).length}
repository`)
);
reaction(
() => this.showError.get(),
() => console.log("something went wrong!!")
);
} fetchData() {
fetch("https://api.github.com/users/t-kalra/repos")
.then(res => res.json())
.then(repos => {
this.setWeatherData(repos);
})
.catch(() => this.setError());
} @action.bound
setWeatherData(data) {
this.repos.push(data);
}@action.bound
setError() {
this.showError.set(true);
}
}
const store = new githubReposList();
store.fetchData();
********************************************************************Prints:
// You have 1 repository
runInAction:
This is a utility function and equivalent to action(fn). This is used to make the asynchronous code execute inside an action().
class cardActivity {
operationState = observable.box("");
activity = observable.array([], { deep: false });constructor() {
reaction(
() => this.operationState.get(),
() => console.log(this.operationState.get())
);
reaction(
() => this.activity.length,
len => console.log("No. of records available:", len)
);
}
@action
async fetchData() {
this.operationState.set("pending");
try {
const response = await this.waitingForData();
runInAction(() => {
this.activity.push(response);
this.operationState.set("completed");
});
} catch (e) {
runInAction(() => {
this.operationState.set("failed");
});
}
}
waitingForData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([
{
id: 1,
name: "mobx"
}
]);
}, 2000);
});
}
}
const ccStore = new cardActivity();
ccStore.fetchData();********************************************************************Prints:
// pending
// No. of records available: 1
// completed
Flow:
In some cases, we need to use multiple await in a function and to update the state on each await success. If we use runInAction on every state-mutation this can create our code uneasy to manage. To make it cleaner, flow
class FlowStore {
@observable operationState = ""; constructor() {
reaction(
() => this.operationState,
data => {
const d = new Date();
console.log(
data,
`${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}`
);
}
);
} fetchData = flow(function*() {
this.operationState = "pending";
yield this.waitingForData(1000);
this.operationState = "initialized";
yield this.waitingForData(1500);
this.operationState = "completed";
yield this.waitingForData(2000);
this.operationState = "reported";
}); waitingForData(delay) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(true);
}, delay);
});
}
}
const store = new FlowStore();
store.fetchData();********************************************************************Prints:
// pending 18:41:3
// initialized 18:41:4
// completed 18:41:6
// reported 18:41:8
Reactions:
Autorun: It takes a function as its input and executes it immediately and also keeps the track of the used variables in the passed-in function. It re-executed when the tracked observables change.
autorun( () => console.log(list.item) )
When: It observes the predicate until its true and once it happen, then execute the function
when( () => this.showModal, () => this.setHeaderDock() );
Reaction: It takes two function, the first function is tracked and return the data that used as an input by the second function. This won’t run directly when created unlike autorun.
reaction( () => this.add, () => console.log() );
Observer: This function/ decorator can be used to turn ReactJS components into reactive components. Any observable entity used in this component, upon change force the re-rendering of the component. This is part of the mobx-react package.
var guestData = observable({
guestNumber: Math.floor(100000 + Math.random() * 700000)
});setInterval(() => {
guestData.guestNumber = Math.floor(100000 + Math.random() *
700000);
}, 4000);@observer
class GuestLogger extends React.Component {
render() {
return (
<span>
<b>Guest Information:</b> Mr.
{this.props.guestData.guestNumber} just logged in.
</span>
);
}
}ReactDOM.render(<GuestLogger guestData={guestData} />, document.getElementById("root"));
********************************************************************Prints:
// Guest Information: Mr. 556068 just logged in.
toJS():
The observable array is not a real JavaScript array. you need to convert this when you are passing it to the other libraries.
const items = observable.array([20]);
const plainArray = toJS(items);
console.log(plainArray);
Decorator:
This is a JavaScript function which is used to modify class properties/methods or class itself. Read this to learn more A minimal guide to ECMAScript Decorators
Decorate():
A clean approach to create the observable.
decorate( target , decorator-object )
class UserInfo {
name = "John";
country = "Brazil";
loginId = Math.floor(100000 + Math.random() * 700000); get userLabel() {
return {
__html: `<a href="/user/${this.loginId}"><b>${this.name}</b>
from ${this.country}</a>`
};
} updateCountry(country) {
this.country = country;
}
}decorate(UserInfo, {
name: observable,
country: observable,
userLabel: computed,
updateCountry: action
});const userInfo = new UserInfo();
console.log(userInfo.userLabel);userInfo.updateCountry("Spain");
console.log(userInfo.userLabel);
********************************************************************Prints:
// {__html: "<a href="/user/404093"><b>John</b> from Brazil</a>"}
// {__html: "<a href="/user/404093"><b>John</b> from Spain</a>"}
extendObservable:
It allows you to mix in additional properties at runtime and make them observable.
class Modal {
user = observable.object({
name: "Tom",
age: 30
}); constructor() {
autorun(() => console.log(this.user, "autorun"));
reaction(() => this.user,()=>console.log(this.user,`reaction
`));
} @action
updateUserInfo(key, value) {
this.user[key] = value;
} setupReaction(key) {
reaction(
() => this.user[key],
() => console.log(this.user[key], `- reaction on ${key}`)
);
}
}
const store = new Modal();setTimeout(() => store.updateUserInfo("designation","DevOps"), 500);
extendObservable(store.user, { gitHubId: "Tomz" });store.setupReaction("designation");
store.setupReaction("gitHubId");setTimeout(() => store.updateUserInfo("designation", "QA"), 1000);
setTimeout(() => store.updateUserInfo("gitHubId", "Philips"), 1500);
********************************************************************Prints:
// {name: "Tom", age: 30, gitHubId: "Tomz"} "autorun"
// NO REACTION FOR "designation" as neither this not part of
initialized observable nor added with extendObservable
// Philips - reaction on gitHubId
Enforce the use of actions:
To avoid the direct update the observable entity.
Automatically updating application state is an anti-pattern — Michel Weststrate
import { configure } from ‘mobx’;
configure({ enforceActions: true });
My Name is Tarun Kalra. I am a Sr. Frontend Developer at Publicis.Sapient
If you enjoyed this article, please clap n number of times and share it! Thanks for your time.