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.
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 @computed
updates), 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.
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.