MobX is another state management library available for React apps besides Redux and Context API. However, MobX is not just a library for React alone, it is also suitable for use with other JavaScript libraries and frameworks that power the frontend of web apps. The MobX >=5 version runs on any browser that supports ES6 proxy.
Major concepts
Here are the major concepts of mobx:
Observable
The observable allows us to turn any data structure or property into an observable state so that other things can keep track of these observable changes and values.
Action
The action allows us to change the state i.e. values of observable. The state data should not be modified outside actions to ensure code scalability.
Computed
The computed property allows us to derive some value based on the state change. The computed values are obtained by performing some sort of calculations on observables.
Observer
The observer allows us to keep track of changes in observables so that React gets notified on any change and starts re-rendering. They are provided by the mobx-react package.
Store
The store is the source of data. Unlike redux, in mobx data and functions which change the data live in the store. So a store may contain observables and actions.
Now let’s put these concepts into practice.
We are going to create a simple application where users can react to images and comment on it, similar to Facebook. Here’s the link to the demo.
Project setup
Assuming prior knowledge of React, you need to have NPM and Node.js installed on your machine.
I am using custom webpack configuration and setting up the project to enable decorators. Don’t worry, there’s an alternative way to do this without decorators as well. For this example, I’m using decorators anyway since it’s more concise. But I’ll mention the alternatives as well. If you’re using create-react-app you can skip these setup steps.
Pull the master
branch from this repository for initial setup.
Run yarn
to install dependencies and start the project using yarn start
. The app will run on http://localhost:8080.
Setup for decorators
The following plugins are required to enable ESNext decorators.
yarn add --dev @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators
Then add the following configuration to .babelrc file.
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
],
[
"@babel/plugin-proposal-class-properties",
{
"loose": true
}
]
]
Styles
Pull design branch for styles. All the styling is under the css
folder inside the src
directory. Here's a visual of our app components.
- Card component with:
Randomly generated image.
Count component to keep track of the number of likes and comments.
Button component with Like and Comment buttons.
Form component with the input field to post a comment.
Comments component with a list of comments.
Installing Dependencies
Install mobx
state management solution and mobx-react
library to connect the state layer to the React view layer.
yarn add mobx mobx-react
Now we will actually start adding features using Mobx.\
Store
First, we’re going to create a Store.jsx
under store
folder.
import { observable, action } from 'mobx'
class Store {
@observable likesCount = 12
@action updateCount{
this.likesCount++;
}
}
const storeInstance = new Store()
export default storeInstance;
Here we’ve created a Store class with likesCount
as an observable state, updateCount
as an action to modify the state and then exported a new instance of the Store.
If your setup doesn’t support decorators the above code can be re-written as:
import { decorate, observable } from "mobx";
class Store {
likesCount = 12;
updateCount{
this.likesCount++;
}
}
decorate(Store, {
likesCount: observable,
updateCount: action
})
Then we make the store accessible throughout the app by passing it using the Context API in main.js
.
import storeInstance from './store/Store'
export const StoreContext = React.createContext();
ReactDOM.render(
<StoreContext.Provider value={storeInstance}>
<Post />
</StoreContext.Provider >
, document.getElementById('app'));
Now we can access the store and its class properties in Count.jsx
using useContext
. Since we've set the initial value of likesCount
to 12, your app will render that value.
import React, { useContext } from 'react';
import { StoreContext } from '../main'
export default function Count() {
const store = useContext(StoreContext)
return (
<div className="row reactions-count" >
<div className="col-sm" align="left">
<i className="fa fa-thumbs-up" />{store.likesCount}
</div>
<div className="col-sm" align="right">
3 comments
</div>
</div>
)
}
Remember that the observable state can only be modified via actions. So in order to increment likesCount
when the user clicks on Like button we are going to use updateCount
action from the store that we've already defined. Handle onClick
action in Buttons.jsx
.
const store = useContext(StoreContext)
<button type="button" className="btn btn-light align-top" onClick={() => store.updateCount()}>
<i className="fa fa-thumbs-o-up" />
Like
</button>
If you click on the Like button you won’t see any changes.
To observe and react to changes in a functional component, we can either wrap the component in observer function or implement useObserver hook, like below. So let’s update Count.jsx
as:
import { useObserver } from 'mobx-react';
...
return useObserver(() => (
<div className="row reactions-count" >
<div className="col-sm" align="left">
<i className="fa fa-thumbs-up" />{store.likesCount}
...
...
</div>
</div>
))
Now the like count updates when you click on the button.
Comments
Let’s start working on the comments section.
An array data structure can be observable as well. Let’s create an observable comments
field. Add the following in Store.jsx
.
@observable comments = ["Wow", "awesome"]
Then access the comments property of Store class from Comments.jsx
like we did before in Count.jsx
using useContext
. The Comments component will now render the comments from the store.
import React, { useContext } from 'react';
import { StoreContext } from '../main';
export default function Comments() {
const store = useContext(StoreContext)
return (
<table className="table">
<tbody>
{
store.comments.map((comment, index) => {
return (
<tr key={index}>
<td>
{comment}
</td>
</tr>
)
})
}
</tbody>
</table>
)
}
We also need to allow the user to add comments from the form.
First, let’s create an action called postComment
in the store that simply pushes the new comment into the previous array of comments. Add the following lines of code in Store.jsx
.
@action postComment(comment){
this.comments.push(comment)
}
Then update the Form.jsx
component as:
import React, { useContext } from 'react';
import { StoreContext } from '../main';
export default class Form extends React.Component {
handleSubmit = (e, store) => {
e.preventDefault();
store.postComment(this.comment.value);
this.comment.value = "";
}
render() {
return (
<StoreContext.Consumer>
{
store => (
<form onSubmit={(e) => this.handleSubmit(e, store)}>
<div>
<input type="text" id={'comment'} className="form-control" placeholder={"Write a comment ..."} ref={node => {
this.comment = node;
}} />
</div>
</form>
)
}
</StoreContext.Consumer>
)
}
}
Here we’ve simply created a function that calls the store’s postComment
action when the user submits the comment and set the input field to empty after submit.
To update the comments component when a new comment is added, we need to make the Comments component an observer as we did with Count. So in Comments.jsx
wrap the content to be returned with useObserver
. Also, don't forget to import useObserver
.
return useObserver(() => (
<table className="table">
<tbody>
{
store.comments.map((comment, index) => {
...
...
}
</tbody>
</table>
)
)
Now if you write any comment and hit enter your list of comments will automatically update.
Let’s focus on the input field when you click on the comment button. We can simply use HTML DOM focus( ) method. But first, let’s give the input field an id.
<input type="text" id={'comment'} className="form-control" placeholder={"Write a comment ..."}
ref={node => {this.comment = node;}} />
Then add focus method on onClick
handler of comment button in Buttons.jsx
component.
<button type="button" className="btn btn-light"
onClick={() => document.getElementById('comment').focus()}>
<i className="fa fa-comment-o" />
Comment
</button>
Now when you click on the comment button the comment field is focused.
Computed
Now in order to get the count of the comments, we are going to create a commentsCount
getter function that computes the observable comments
array's length. MobX will ensure commentsCount
updates automatically whenever comments
array changes. In Store.jsx
add the following:
@computed get commentsCount(){
return this.comments.length;
}
Then simply update the following lines in Count.jsx
.
<div className="col-sm" align="right">
{store.commentsCount} comments
</div>
You’ll also notice that when you add a comment, the count gets updated as well.
Services / API call
Making an API call and asynchronous codes are frequent in applications. Since this is a custom webpack configuration to enable async/await update the .babelrc
file with the following.
"presets": [
["@babel/preset-env",
{
"targets": {
"node": "10"
}
}
],
"@babel/preset-react"
],
or else you might run into this error
Let’s change the image in the Card.jsx
component on button click. We are going to use this fun and free API to fetch the characters' images from the Rick and Morty show. Check out their documentation for more details.
You’ll find from this section that we can get a single character by adding the id
as a parameter: /character/1
https://rickandmortyapi.com/api/character/1
Let’s create an image store with observable imageUrl
containing default value. Then we create a fetchImage
action that returns the JSON response of a single character.
After await
a new asynchronous function is started, so after each await
, state modifying code should be wrapped as action. There are multiple ways to do this. Read this section of Mobx documentation for more details.
One way is to use the runInAction
, which is a simple utility that takes a code block and executes in an anonymous action. Here we are wrapping the state modifying part after await in runInAction
.
import { action, runInAction, observable } from "mobx";
class ImageStore {
id = 1
@observable imageUrl = `https://rickandmortyapi.com/api/character/avatar/1.jpeg`
@action async fetchImage() {
const characterId = ++this.id
const response = await fetch(`https://rickandmortyapi.com/api/character/${characterId}`)
const data = await response.json()
runInAction(() => {
this.imageUrl = data.image
})
}
}
const imageStore = new ImageStore()
export default imageStore;
You can also run only the state modifying part of the callback in an action. Here we’ve created an action to set the URL outside the fetchImage
and then called it as required.
class ImageStore {
...
@action async fetchImage() {
...
this.setImageUrl(data.image)
}
@action setImageUrl(url) {
this.imageUrl = url
}
}
Then in Card.jsx
component
Import the
imageStore
and set the source of the image to the observableimageUrl
from the store.Implement
useObserver
to react to changes.Add a button with
onClick
handler that calls thefetchImage
to get the image URL.
import React from "react";
import Count from "./Count";
import Buttons from "./Buttons";
import imageStore from '../store/ImageStore'
import { useObserver } from "mobx-react";
export default function Card() {
return (
useObserver(() => (
<div className="card">
<img src={imageStore.imageUrl} className="card-img-top" alt="..." />
<button className="btn btn-light" onClick={() => { imageStore.fetchImage() }}>
<i className="fa fa-chevron-right" />
</button>
<Count />
<div className="card-body" >
<Buttons />
</div>
</div>
))
);
}
Aaand we’re done! Here’s what your final output will look like:
#Note
The nice thing about bundling actions with stores is that we can use them in onClick
handlers. Which means most of the components, like in this example, can be stateless functional components. To make a class component an observer we can use @observer
decorator or wrap the component with observer
function.
import React from "react";
import { observer } from "mobx-react";
//With decorator
@observer
export default class Form extends React.Component{
...
}
//Without decorator
class Form extends React.Component{
...
}
export default observer(Form)
Mobx docs are well-written and contain a lot of best practices.
You can find all the code of this example here —
And that’s it. Thanks for reading! 😃
Top comments (5)
Can you use Mobx without classes nor decorators, just functions?
Decorators are optional. I have mentioned the alternatives without decorators in the article.
And with the React 16.8 Hooks update, 'hooks let you use more of React’s features without classes'. And you can easily use Mobx with functional components ( no classes, no decorators ).
For more details, you can read this section of mobx docs.
how do you create a input table and dynamically add more rows if needed ? and save all data to mobx?
If you look at the ‘adding comments’ portion of the article, you can do it in a simliar way.
For your case, create an observable rows array and an action creator to add more rows.
@observable rows = [“”]
@action addRows() { this.rows.push(“”)}
Use addRows to dynamically add more rows.
Thank you so much for this... Been looking for a tut for MobX and this hits the nail on the head perfectly!