January 07, 2020
As we all know state management is the most important and core part of any react application and as react is just a UI library there is a need for something to take care of the state of our app. State management can be troublesome and it is easy to create unmanageable react applications because of inconsistent state.
A state is just the data that your app is dealing with. State saves the data that needs to be rendered and it influences how the component gets rendered. State management is the process of managing that data. Monitoring and retrieving data in a particular app can be difficult that's where state management libraries come into play. There are multiple ways to manage states like Redux and Context but we will talk precisely about MobX here.
MobX is a simple, scalable and battle-tested state management solution. It's a standalone library that can be used with any javascript framework. React and MobX is really powerful together and work as a complete framework. MobX provides the mechanism to store and update the application state that React then uses to render the components.
MobX at its core has three important concepts: Observables, Actions, and Reactions. A Store contains these three which then is used by the React application.
Find minimal state (observable state), derive everything (derived state) and never turn the state into more state.
The observable state is one of the main concepts of MobX. The idea behind this concept is to make an object able to emit new changes on them to the observers. You can achieve this with the @observable
decorator.
For example, imagine you have a variable named firstName that you expect to change on any event. You can make it observable like so:
//import observable from mobx
import { observable } from "mobx";
//create a store with count observable
class Store {
@observable
firstName = '';
}
//export Store
export default new Store();
The state of your application consists of a core-state
and derived-state
. The core-state is state inherent to the domain you are dealing with.
The computed value is another important concept of MobX. These values are represented by the @computed
decorator. Computed values work in hand with observable states. These values can be derived from observables. For example:
import { observable, computed } from "mobx";
class Store {
@observable
firstName = '';
@observable
lastName = '';
@computed
get fullName = () => this.firstName + ',' + this.lastName;
}
export default new Store();
Here, @computed
is working as a getter function deriving its value from firstName
and lastName
. fullName
will emit changes as the value of firstName
and lastName
changes.
MobX supports a uni-directional data flow where actions change the state, which in turn updates all affected views.
An action is any piece of code that modifies the state.
You can mark your actions using the @action
decorator. As such, you are supposed to use the @action
on any function that modifies observables or has side effects. A simple example can be:
Store {
import { observable, computed, action } from "mobx";
class Store {
@observable
firstName = '';
@observable
lastName = '';
//action that receives text and updates firstName observable
@action
setFirstName = text => {
this.firstName = text;
}
@computed
get fullName = () => this.firstName + ',' + this.lastName;
}
export default new Store();
Reactions are very similar to computed values. The difference here is that, instead of computing and returning a value, a reaction simply triggers a side effect, more like it performs a side operation. In simple words, Reactions are:
Side effects that should occur in reaction to state changes (component re-render)
Reactions occur as a result of changes in observables. Reactions could affect the UI, or they could be background actions. MobX provides three main types of reaction functions: autorun, when and reaction.
When autorun
is used, the provided function will always be triggered once immediately and then again each time one of its dependencies changes. autorun
can be used in those cases where you want to create a reactive function that will never have observers itself. This is usually the case when you need to bridge from reactive to imperative code, for example for logging, persistence, or UI-updating code.
autorun( reaction => {
/* do some stuff */
reaction.dispose();
} );
The return value from autorun is a disposer function, which can be used to dispose of the autorun when you no longer need it.
when
observes & runs the given predicate until it returns true. Once that happens, the given effect is executed and the autorunner is disposed. The function returns a disposer to cancel the autorunner prematurely.
This function is really useful to dispose of or reactively cancel stuff. For example:
class MyResource {
constructor() {
when(
// once...
() => !this.isVisible,
// ... then
() => this.dispose()
);
}
@computed get isVisible() {
// indicate whether this item is visible
}
dispose() {
// dispose
}
}
A variation of autorun
which takes two functions: the data function and the effect function. The first one (the data function) is tracked and returns data that is used as input for the second one, the effect function.
Unlike autorun, the side effect runs only after the data expression returns a new value for the first time.
The second function (the effect function) passed to reaction will receive two arguments when invoked. The first argument is the value returned by the data function. The second argument is the current reaction, which can be used to dispose of the reaction during execution.
In the following example, reactionDemo will react to the change in the counter count. When invoked reaction, the second argument can use as a disposer. The following example shows a reaction that is invoked only once.
const counter = observable({ count: 0 });
// invoke once of and dispose reaction: reacts to observable value.
const reactionDemo = reaction(
() => counter.count,
(count, reaction) => {
console.log("reaction demo: invoked. counter.count = " + count);
reaction.dispose();
}
);
counter.count = 1;
// prints:
// reaction demo: invoked. counter.count = 1
counter.count = 2;
// prints:
// (There are no logging, because of reaction disposed. But, counter continue reaction)
console.log(counter.count);
// prints:
// 2
We will understand the working of MobX by creating an application in three simple steps
The application uses
openlibrary.org
API to fetch books by searching author names and adding them to favorites. See code: https://codesandbox.io/embed/mobx-demo-69c47
We are going to search for the author's name and save the result. Then we will click on books and add them to our favorites. Comments are self-explanatory:
//importing observables and decorate
import { decorate, observable } from "mobx";
class Store {
//text to save author name to search
text = "";
//data to save response from api
data = null;
//addedBooks to save favourite books
addedBooks = [];
}
//another way to decorate variables with observable
decorate(Store, {
text: observable,
data: observable,
addedBooks: observable,
});
//export class
export default new Store();
Create the SearchBooks
component that will accept the author's name as text input and show the API response as a list. It will also show the number of books added and their names.
import React from "react";
//import inject and observer from 'mobx-react'
import { inject, observer } from "mobx-react";
//components receive Store values as props which we will inject while exporting
function SearchBooks(props) {
return (
<div>
//component fields goes here
</div>
);
}
//inject Store as props to SearchBooks and make it observe to changes in Store
export default inject("store")(observer(SearchBooks));
input
to accept text and button
to call APIreturn (
<div>
<label>Search books</label>
<input
type="text"
value={props.store.text} /*text observable defined in Store*/
placeholder="Type Author name"
onChange={e => {/*Action to update text*/}}
/>
<button className="button" onClick={e => {/*Action to call api*/}}>
Search
</button>
</div>
);
openlibrary.org
returns a response with docs
array containing all the books. We will map that array to show a list of booksreturn (
//...input field previously added
//observable data variable
{props.store.data.docs.map((item, i) => {
return (
<li key={item.key} onClick={() => {/*Action to add books to favourite*/}}>
//Prints as 1. Book Title
{i + 1}. {item.title_suggest}
</li>
);
})}
);
Go to openlibrary.org to learn more about api response.
Now we will create actions that will update text, data, and addedBooks observables. Also, we will add a computed state to get the total number of books added.
import { decorate, observable, action, computed } from "mobx";
class Store {
text = "";
data = null;
addedBooks = [];
//action to update text
setText = function(text) {
this.text = text;
};
//action to search books
searchBooks = function() {
fetch(`https://openlibrary.org/search.json?author=${this.text}`)
.then(response => response.json())
.then(data => {
//calling action to update data
this.setData(data)
});
};
//action to update data
setData = data => {
this.data = data;
};
//action to add books to favourite upon click
addBook = function(book) {
this.addedBooks.push(book);
this.data = null;
this.text = "";
};
//computed value to total books added
get totalBooks() {
return this.addedBooks.length;
}
}
//decorate action and computed values
decorate(Store, {
text: observable,
data: observable,
addedBooks: observable,
setText: action,
searchBooks: action,
setData: action,
addBook: action,
totalBooks: computed
});
export default new Store();
Now we will update our View for these added actions.
return (
<div>
<label>Search books</label>
<input
type="text"
value={props.store.text}
placeholder="Type Author name"
onChange={e => {
//action to update text
props.store.setText(e.target.value)
}}
/>
<button className="button" onClick={e => {
//action to call api and search books
props.store.searchBooks()
}}>
Search
</button>
//show total books added with computed value
<h1 className="bookcount">Books added: {props.store.totalBooks}</h1>
//toggle between search reponse and addedBooks when data is available or null.
{props.store.data
//when data has search result, render a list of books by author
? props.store.data.docs.map((item, i) => {
return (
<li key={item.key} onClick={() => {
//action to add a book to favourite
props.store.addBook(item)
}}>
{i + 1}. {item.title_suggest}
</li>
);
})
//when data is null, show a list of books added by user
: props.store.addedBooks.map((item, i) => {
return (
<li id="added" key={item.key}>
{i + 1}. {item.title_suggest}
</li>
);
})}
</div>
);
Now, the last thing we have to do is provide store in Provider
to the root component. Our root file will look like this:
import React from "react";
import ReactDOM from "react-dom";
//importing Provider, store and SearchBooks component
import { Provider } from "mobx-react";
import store from "./store";
import SearchBooks from "./SearchBooks";
import "./styles.css";
//provide store as a provider so that our component can use it.
function App() {
return (
<Provider store={store}>
<SearchBooks />
</Provider>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
That's it. We are done and you can view this project in action at this link. I hope this tutorial was able to get you started with MobX. Happy coding!