If you've ever used Redux for managing state in your web application, you'll know how much boilerplate you have to write before you can mutate its state. Sure, there's plenty of Redux middleware for reducing boilerplate and making asynchronous requests (such as XHR). However, the more middleware you add, the more mental overhead you'll have to deal with.

I like doing things the JavaScript way; it just makes life so much easier for my team and I. If you're having to hack around some weird issue in JavaScript, someone has probably already solved the issue, but their solution has gotten lost in the vast JavaScript ecosystem. That is unless you're doing some insane next level stuff. Why write your own left padding function when you can just use left-pad? Why bother writing a backend for some generic CRUD dashboard if you can just use Parse?

Anyways, I digress. Up until a couple months ago, I had the illusion that Redux was the only mainstream state management plugin for React. Then my team told me about MobX. Instead of the fancy action/reducer pattern Redux uses, MobX simply makes your data observable. Your app will react anytime your data is mutated.

Motivation

In order to motivate the rest of this article, let me provide examples for both MobX and Redux.

import { createStore, dispatch } from 'redux';

// Command
const ADD_TODO = 'ADD_TODO';

// Action Creator
function addTodo(text) {
  return {
    type: ADD_TODO,
    text,
  };
}

// Reducer
function rootReducer(state = { text: '' }, action) {
    return {
        ...state,
        text: action.text,
    };
}

// Store
const store = createStore(rootReducer);

// Every time the state changes, log it
const unsubscribe = store.subscribe(() => console.log(store.getState()));

// Output: { text: 'dubemon' }
dispatch(addTodo('dubemon'));

unsubscribe();
import { observable, autorun } from 'mobx';

class Store {
    @observable text = '';
}

const store = new Store();

// Every time the state changes, log it
const disposer = autorun(() => console.log(store));

// Output: { text: 'dubemon' };
store.text = 'dubemon';

disposer();

Wow, MobX has way less boilerplate. Case closed. Thanks for reading!

That was my first thought when I saw MobX, but I wouldn't be writing this article if that were the case. Unlike Redux, MobX is completely unopinionated and simply makes your data observable. It's up to you to structure your data, which can lead to maintainability issues if you're inexperienced.

The biggest concept to keep in mind is MobX pairs well with object oriented programming (OOP).

The one thing MobX and Redux encourage is keeping most of your application's state in a single store. Having a single store allows you to save your application's state in snapshots. Snapshots make time travel debugging possible where you can see how the application's state changed over time. You can also persist snapshots so that a user's session can be reloaded even if they leave the page.

Data Modelling

Let's look at an example of a Store containing a collections of Todo objects. First we define our OOP data models: Todo. Then, we create our Store state, which is a collection of Todo objects.

class Todo {
	@observable text = '';
}

class Store {
	@observable todos = [];
}

Rather than allowing users to directly modify Todo's internal text property, we should instead provide a setText method in case we ever want to add validation or transformation logic. setText is decorated with action to make it clear that it mutates the state, but this is only cosmetic.

class Todo {
	@observable text = '';

	@action
	setText() {
		this.text = text;
	}
}

Data Collections

Managing collections in MobX becomes tricky. If you replace the Todos array with a new array, observers watching for changes to todos will be stuck watching the old array and won't pick up on changes to the new array.  The following snippet is a contrived example of how to modify the Store's collection without breaking its observers.

class Store {
	@observable todos = [];

	// BAD: todos is re-assigned
	addTodo(todo) {
		this.todos = [...this.todos, todo];
	}

	// GOOD: todos is mutated in place
	addTodo(todo) {
		this.todos.push([]);
	}

	// BAD: todos is re-assigned
	removeTodo(index) {
		this.todos = [...this.todos.slice(0, index), ...this.todos.slice(index + 1)];
	}

	// GOOD: todos is mutated in place
	removeTodo(index) {
		// Modify the array with
		this.todos.splice(index, 1);
	}
}

Let's say we want to keep track of the Todo we're currently editing. We can add a property for the current Todo and its index in the Todos array.

@observable currentTodoIndex = 0;
@observable currentTodo = null;

setCurrentTodo(index) {
	this.currentTodo = this.todos[index];

	// It's likely we'll also need to access the index at some point
	this.currentTodoIndex = index;
}

getCurrentTodo() {
	return this.currentTodo;
}

getCurrentTodoIndex() {
	return this.currentTodoIndex;
}

The downside of this approach is now we're storing two additional pieces of state that more or less describe the same object. An alternative to this approach is to take advantage of MobX's computed properties and create a getter.

@observable currentTodoIndex = 0;

setCurrentTodo(index) {
	this.currentTodoIndex = index;
}

// Access this property using store.currentTodo 
@computed get currentTodo() {
	return this.todos[this.getCurrentTodoIndex()];
}

// Alternatively, we can just use a plain old function, 
// but this won't update observers
getCurrentTodo() {
	return this.todos[this.getCurrentTodoIndex()];
}

getCurrentTodoIndex() {
	return this.currentTodoIndex;
}

Asynchronous Actions

The examples we've gone through so far have been fairly straightfoward. However, at some point, you'll probably have to make HTTP or some other type of asynchronous requests. We're going to add mobx-utils to our project to help us in dealing with these async functions. Specifically, mobx-utils has a fromPromise function that wraps a promise that makes it easy to get its current state or observe changes to its state.

import React, { Component } from "react";
import { render } from "react-dom";
import { observer, Provider, inject } from "mobx-react";
import { observable, autorun } from "mobx";
import { fromPromise } from "mobx-utils";

class Store {
	@observable counter = null;

	getCounter() {
		// Get the current value of the counter promise
		// null if the promise is pending
		return this.counter.value;
	}

	fetchData() {
		// Pretend Promise.resolve is making an HTTP request
		this.counter = fromPromise(Promise.resolve(2));
	}

	fetchError() {
		// Pretend Promise.reject is a failed HTTP request
		this.counter = fromPromise(Promise.reject(3));
	}
}

// Allow our React component to observe our Store's state
@inject(({ store }) => ({ store }))
@observer
class Child extends Component {
	render() {
		const { store } = this.props;
		
		// We can display the promise's current state
		switch (store.counter.state) {
			case "pending":
			return <div>Loading...</div>;
			case "rejected":
			return <div>Ooops... {store.counter.value}</div>;
			case "fulfilled":
			return <div>Gotcha: {store.counter.value}</div>;
			default:
			return <div>Loading...</div>;
		}
	}
}

class App extends Component {
	store = new Store();

	componentDidMount() {
		// Child will show the pending then the fulfilled state
		this.store.fetchData();

		// Child will show the pending then the rejected state
		setTimeout(() => this.store.fetchError(), 1000);
	}

	render() {
		return (
			<Provider store={this.store}>
				<Child />
			</Provider>
		);
	}
}

render(<App />, document.getElementById("root"));

If you ever find yourself mutating the same property in an asynchronous function multiple times, use asynchronous flows to ensure your observers correctly receive the mutations.

Splitting Your Store

Now you know patterns for creating a store, setting and getting your observable properties, and dealing with asynchronous functions. You have bigger dreams than that though.

DON'T GET TOO CARRIED AWAY

It's not unreasonable to think you'll have 50 observable collections in your store. However, keeping all of these in your root store is unreasonable. Instead, your root store can contain sub-stores. Generally your sub-stores should not rely on each other, but maybe you didn't fully plan things through like I did. If a store has to observe another store, don't do that in the constructor because the other store may not be initialized yet. Instead, create a setup function that will run after the constructor.

import { observable, autorun } from "mobx";

class RootStore {
	store1 = new Store1(this)
	store2 = new Store2(this)

	constructor() {
		this.store1.setup();
	}
}

class Store1 {
	@observable counter = 1;

	constructor(rootStore) {
		this.rootStore = rootStore;
		autorun(() => console.log(this.rootStore.store2.counter));
		// Uncaught exception since store2 is undefined
	}

	setup() {
		autorun(() => console.log(this.rootStore.store2.counter));
		// Output: 2 \n 3
	}
}

class Store2 {
	@observable counter = 2;

	constructor(rootStore) {
		this.rootStore = rootStore;
	}
}

const store = new RootStore();
store.store2.counter = 3;

Pitfalls

I wish I was a masterful enough writer to weave in the following pitfalls into the rest of the article. One day I'll get there. In the meantime, enjoy the fruits of my painful debugging labour.

MY FACE WHEN DEBUGGING THESE PITFALLS

If you define a function, which mutates state as an @action, any mutations inside that function will not trigger observers until the entire action is completed.

import { observable, action, autorun } from "mobx";

class Store {
  @observable counter = 1;

  @action actionCount() {
    this.counter = 0;
    this.counter = 1;
  }

  count() {
    this.counter = 0;
    this.counter = 1;
  }
}

const store = new Store();
autorun(() => console.log(store.counter));
// Output: 1
store.actionCount();
// Output: 1
store.count();
// Output: 0 1

It is possible to nest non-primitive data types and observe the inner types, but there are some pitfalls. You can nest non-observable types, because they will automatically be observed once added to their parent, but keep in mind that they will not be observable before then.

import { observable, autorun } from "mobx";

class Store {
    @observable map = new Map();

    // Not an @action since we want to watch changes as they happen
    actionCount() {
        const arr = [];
        // Triggers autorun on line 17 and adds an observer
        this.map.set('key', arr);
        arr.push(1);
        
		// No output
        this.map.get('key').push(2);
        
		// Output: { ..., added: [2] }
	}
}

const store = new Store();
autorun(() => store.map.get('key') &&
	store.map.get('key').observe(console.log)
);
store.actionCount();

I hope you learned something about MobX today. I've only been using it personally for two months and am sure there's better patterns than what I've described here. I learned a lot from experimenting in CodeSandbox and by reading all the other great (but often outdated or not idiomatic) resources on awesome-mobx.

Click here if you want to see a simple React MobX project I built that implements some of these principles.