DEV Community

Rosy Shrestha
Rosy Shrestha

Posted on • Edited on

Build your first app with Mobx and React

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.

Very in a nutshell mobx works on the observable — observer flow. You declare some data to be observable and when that data changes all the observers that are using that data will be notified.

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
Enter fullscreen mode Exit fullscreen mode

Then add the following configuration to .babelrc file.

"plugins": [
        [
            "@babel/plugin-proposal-decorators",
            {
                "legacy": true
            }
        ],
        [
            "@babel/plugin-proposal-class-properties",
            {
                "loose": true
            }
        ]
    ]
Enter fullscreen mode Exit fullscreen mode

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:
  1. Randomly generated image.

  2. Count component to keep track of the number of likes and comments.

  3. 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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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'));
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
    ))
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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>
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
    )
    )
Enter fullscreen mode Exit fullscreen mode

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;}} />
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Then simply update the following lines in Count.jsx.

<div className="col-sm" align="right">
      {store.commentsCount} comments
</div>
Enter fullscreen mode Exit fullscreen mode

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"
    ],
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

Then in Card.jsx component

  • Import the imageStore and set the source of the image to the observable imageUrl from the store.

  • Implement useObserver to react to changes.

  • Add a button with onClick handler that calls the fetchImage 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>
        ))
    );
}
Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
ndrean profile image
NDREAN

Can you use Mobx without classes nor decorators, just functions?

Collapse
 
rosyshrestha profile image
Rosy Shrestha

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 ).

const person = observable({
  name: 'John Doe'
})

person.observe(...)
Enter fullscreen mode Exit fullscreen mode
// wrap the component in observer function from mobx-react package
const Function = observer(()) => {....}

// or use useObserver hook from mobx-react package
const Function = useObserver (()) => {....}

Enter fullscreen mode Exit fullscreen mode

For more details, you can read this section of mobx docs.

Collapse
 
bishwobhandari profile image
bishwobhandari

how do you create a input table and dynamically add more rows if needed ? and save all data to mobx?

Collapse
 
rosyshrestha profile image
Rosy Shrestha

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.

Collapse
 
johnnyxbell profile image
Johnny Bell

Thank you so much for this... Been looking for a tut for MobX and this hits the nail on the head perfectly!