State Management with MobX and React

Nick Brown JavaScript, Problem Solving, React, Technology Snapshot Leave a Comment

There are many options when it comes to managing the state of a React application. Choosing the right one for your application can feel daunting.

The most popular choice, Redux, is often thought of as verbose because it requires a lot of boilerplate code, thus slowing down development. Redux is also very opinionated so it will take time for those unfamiliar with its functional programming paradigm to become comfortable with it.

In this blog, we’ll take a closer look at an alternative that aims to solve issues in React application state: MobX. To do that, I built a simple calorie counter application that will be used to showcase MobX in use. You can see the application running here.

Getting Started

To get started using MobX in your React application, install the library itself and another with the tools for utilizing it with React: MobX & MobX-React.

If you want to use the fancy decorator syntax, you will also need to install an additional Babel plugin babel-plugin-transform-decorators-legacy and add it to your Babel plugins. If you use create-react-app to scaffold your project, there are a few ways to add support for decorators. The simplest is to eject your configuration and add the plugin to the Babel config in your package.json.

npm run eject

...
"babel": {
   "plugins": [
     "babel-plugin-transform-decorators-legacy"
   ],
   "presets": [
     "react-app"
   ]
 }
...

It should be noted that decorators in JavaScript are still a proposal and are subject to change. Their use with MobX is optional, but choosing not to use them means you miss out on one of MobX’s greatest strengths: minimal boilerplate. They also make your code much more readable. For examples of using MobX without decorators, check out their documentation.

To make use of MobX devtools, install mobx-react-devtools and render it in one of your React components. When enabled, this will provide a small toolbar in the upper right-hand of your application. There is an option to log all state mutations to the console, another for highlighting renders, and one that allows you to click on a component in the dom to show its dependency tree.

Creating a MobX Store

A MobX store is just a class that holds the state of your application in observables. It makes updates to those observables through actions and derives data from your observables through computed values. Mutations in the data will cause reactions, like updating the dom.

Let’s take a look at a simplified version of the MealsStore used to drive the state of the calorie counter application.

import { observable, action, computed } from 'mobx';
import MealStore from './MealStore';

class MealsStore {
   @observable
   addMealName = '';

   @observable
   addMealCalories = 0;

   @observable
   mealDate = new Date(new Date().setHours(0,0,0,0));

   @observable
   meals = [];

   @action
   addMeal = () => {
       this.meals.push(
           new MealStore({
               name: this.addMealName,
               calories: this.addMealCalories,
               date: this.mealDate.toDateString()
           })
       );
       this.addMealCalories = 0;
       this.addMealName = '';
   };

   @computed
   get caloriesForDay() {
       return this.mealsForDay.reduce(
           (total, meal) => { return meal.calories + total; },
           0
       );
   };
};

export default MealsStore;

@Observable

Observables are the state of our application. They can be primitives, objects, arrays, and behave pretty much like you’re used to with JavaScript objects and arrays. Changes to observables are tracked through property access and updates to them will cause your react components to re-render.

See Also:  What's New in JUnit 5.1

The addMealName and addMealCalories will contain the state of a text box used to define the new meal the user is tracking. The meals array will hold all of the meals to be traced. Data for an individual meal is kept in a separate class MealStore which MealsStore will create instances of and add to meals as the user adds more of them.

@Action

Actions are functions that modify your state. State in MobX is mutable, so no need create a copy of your state for every update like Redux. Instead, it directly alters the existing state with the desired changes. By default, there’s nothing preventing changes to the state outside of actions, but it can be configured to enforce changes from actions.

Using @action can provide some useful debugging data through dev-tools to help track down mutations. It can also help with performance as it allows MobX to batch mutations to observables and update those observing them once the final one is complete.

Asynchronous actions, like loading JSON from an API, can make use of a second @action function to make the mutations on the resolve. If you do not use arrow functions, you can use @action.bound to bind this automatically.

   @action
   loadMeals = () => {
       if (this.isLoaded) {
           return;
       }
       MealsUtil.GetMeals().then(this.loadMealJson);

       this.meals.push(new MealStore({name: 'Ice Cream', calories: 100}));
   };

   @action
   loadMealJson = mealJson => {
       var mealStores = [];

       mealJson.forEach(meal => {
           mealStores.push(new MealStore(meal));
       });

       this.meals = mealStores;
   };

@Computed

Computed is used to generate values that can be derived from your state’s observables. They will run only when a @observable they track is updated and notify those observing the computed when its value has changed. These are handy for doing things like sorting, filtering, or making the state’s data structure easier to handle on the component side.

The mealsForDay computed filters the meals down to only the day the user is currently viewing. Another computed caloriesForDay makes use of the previous to compute total calories tracked for the day.

   @computed
   get mealsForDay() {
       return this.meals.filter(meal => {
           return isSameDay(this.mealDate, meal.date);
       });
   };

   @computed
   get caloriesForDay() {
       return this.mealsForDay.reduce(
           (total, meal) => { return meal.calories + total; },
           0
       );
   };

Usage With React

First, we need to get our store to a component that needs it. We can use a Provider which makes use of React’s context API to allow our store to be injected anywhere in our component tree.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App';
import MealsStore from './stores/MealsStore';
import { Provider } from 'mobx-react';
import DevTools from 'mobx-react-devtools';


const mealsStore = new MealsStore();

ReactDOM.render(
   <Provider mealsStore={mealsStore}>
       <React.Fragment>
           <App />
           <DevTools />
       </React.Fragment>
   </Provider>,
   document.getElementById('root'));

@Inject

Now, for any component, we can use the @inject decorator to have the store passed-in as a prop.

this.props.mealsStore

import React, { Component } from 'react';
import {inject, observer} from 'mobx-react';


@inject('mealsStore')
@observer
class MealTable extends Component {
	...
};


export default MealTable;

@Observer

Any component that needs to update with state changes (like when a @observable mutates or a @computedupdates), will need to be decorated with @observer. This tells MobX to track the render methods property access to computed and observables in our store and re-render when mutations occur.

Observables should be accessed directly from the render method or through functions called by render, as opposed to storing them locally. In the MealTable component, we’ll create a table that makes use of our @computed mealsForDay to show only the meals logged for the current day.

render() {
       const meals = this.props.mealsStore.mealsForDay;
       const deleteButtonDisabled = !this.props.mealsStore.someMealsSelected;

       return (
           <React.Fragment>
               
<Table>
                   <TableHead>
                   <TableRow>
                       <TableCell padding="none" style={{maxWidth: '1em'}}>
                           <IconButton variant="fab" aria-label="Delete Selected" onClick={this.handleDelete} disabled={deleteButtonDisabled}>
                               <Icon>delete</Icon>   
                           </IconButton>
                       </TableCell>
                       <TableCell>Meal</TableCell>
                       <TableCell numeric>Calories</TableCell>
                   </TableRow>
                   </TableHead>
                   <TableBody>
                   {meals.map(meal => {
                       return (
                           <MealTableRow meal={meal} key={meal.id}/>
                       );
                   })}
                   </TableBody>
               </Table>

               {meals.length > 0 ? undefined: 
<div>No meals added for this day!</div>

}
           </React.Fragment>
       )
   };

As new meals are added or deleted, this component will re-render making the necessary updates. Another component AddMeal is used to gather user inputs for meal names and calories and invoking the addMeal action of our store.

import React, {Component} from 'react';
import {inject, observer} from 'mobx-react';
import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';
import AddIcon from '@material-ui/icons/Add';
import Grid from '@material-ui/core/Grid';

@inject('mealsStore')
@observer
class AddMeal extends Component {

   handleMealNameChange = e => {
       this.props.mealsStore.setAddMealName(e.target.value);
   };
  
   handleMealCalorieChange = e => {
       let calories = parseInt(e.target.value, 10) || 0;
       if (calories > 10000) {
           calories = 10000;
       }
       this.props.mealsStore.setAddMealCalories(calories);
   };

   canAddMeal = () => {
       return this.props.mealsStore.addMealName && this.props.mealsStore.addMealCalories
   };

   handleKeyDown = e => {
       if (e.keyCode === 13) {
           this.addMeal();
       }
   };

   addMeal = () => {
       if (this.canAddMeal()) {
           this.props.mealsStore.addMeal();   
       }
   };

   render() {
       const addMealEnabled = this.canAddMeal();
       const addMealName = this.props.mealsStore.addMealName;
       const addMealCalories = this.props.mealsStore.addMealCalories;

       return (
           <Grid item xs={12} spacing={8} container justify="space-evenly">
               <Grid item xs={4}>
                   <TextField label="Meal Name" inputProps={{maxLength:50}} value={addMealName} placeholder="Ice cream..." onChange={this.handleMealNameChange} onKeyDown={this.handleKeyDown} InputLabelProps={{shrink: true}} fullWidth={true}/>
               </Grid>
               <Grid item xs={4}>
                   <TextField type="number" label="Calories" inputProps={{step: 50, min: 0}} value={addMealCalories} onChange={this.handleMealCalorieChange} onKeyDown={this.handleKeyDown} InputLabelProps={{shrink: true}} fullWidth={true} />
               </Grid>
               <Grid item xs={1}>
                   <Button variant="fab" color="primary" disabled={!addMealEnabled} aria-label="Add Meal" onClick={this.addMeal}>
                       <AddIcon />
                   </Button>
               </Grid>
           </Grid>
       );
   };

};

export default AddMeal;

For more examples, get the full source code here, or see it running on CodeSandbox.

See Also:  Angular Developer: JavaScript to TypeScript

Performance

In my experience, performance with MobX and React is pretty good without a lot of effort. There are a few general rules to follow if you find you need to make improvements:

  • Break down large components into smaller ones.
  • The fewer observables a component needs to access, the less it will need to render pieces of your application unnecessarily.
  • Access the data as late as possible.
  • Instead of passing an observable string to a child as a prop, pass the object and have the child access the string.
  • Only the child will need to re-render on mutations instead of the parent.

Final Thoughts

I’ve enjoyed my time using MobX. It is fun to use and very simple to get the hang of. It is relatively un-opinionated, allowing the developer to structure data and code in a way that is familiar to them. Developers with object-oriented backgrounds will feel right at home.

Those looking to add external state management to their application, but are cautious of the complexity or verboseness of other options should consider giving MobX a try.

What Do You Think?