<onWebFocus />

Knowledge is only real when shared.

Simply MobX

August 30, 2021

Introduction to state management with MobX.

React's model where a components is rerendered every time some data changes requires a state management library when data is to be shared between components and all components using said data should rerender automatically.

Although Redux and MobX were created at pretty much the same time, it was Redux that gained huge popularity early on and is still more popular. Redux is from an implementation standpoint very simple and once the basic model is understood is quite easy to use. However, it requires you to do a lot of tasks manually and therefore write more code for the same functionality.

It's still recommended to start with Redux but after a while move on to MobX which unfortunately has quite a steep learning curve and requires more experience to do what you want it to. The following introduction aims to get you started smoothly with whichever starting point you want to take.

Concepts

State management has the goal to store data in the form of any basic JavaScript types and also to inform consumers (usually React components) about changes to the data they have access to.

Informing each consumer about every change to the data isn't feasible from a performance perspective. Therefore, state management tools have implemented different approaches on how to only inform the necessary consumers about changes. Of course the approach taken doesn't really affect what is easier to use, but is important to understand how to use it.

Propagating Changes

Both Redux and MobX require you to wrap any component accessing state with connect or observer. Without this Higher-Order Component wrapped around changes to the state will not result in a rerender of components. When connecting a component to Redux you have to manually specify all the properties of the state that the component can have access to. Redux passes these properties anew to the component every time something on the state changes. However, components will perform a shallow compare and only rerender if anything has changed. MobX on the other hand will not require you to manually specify which data should be passed to the component and a rerender will only happen if any of the data accessed has changed.

Performance

From a performance perspective both Redux and MobX are great. The performance can however suffer greatly if not implemented properly. Making sure that components stay small and only have access to specific parts of the state is most important and will usually resolve any performance related issues. In theory Redux is slightly faster than MobX however requiring the user to do everything by hand which significantly decreases developer performance. Performance should be considered a minor factor when deciding to use Redux or MobX.

Immutability with Redux

This post is mainly about MobX but due to the prevailing popularity of the immutability approach with Redux it's concept will be explained in the following section that is shown by clicking Expand.

Observable with MobX

Below is an example of a dynamic React component displaying data from a MobX observable store. The following paragraphs will describe in detail what's happening.

import { observer } from 'mobx-react-lite'
import { Data } from './Data'

export default observer(() => (
  <div>
    <h1>{Data.counter.name}</h1>
    <p>Count: {Data.counter.count} Double: {Data.double}</p>
    <button onClick={Data.increment}>increment</button>
    <button onClick={() => Data.increment(2)}>increment by 2</button>
  </div>
))

The observable approach works differently in that it augments the data so that it can track which parts are accessed or updated. For normal usage the augmented data isn't distinguisable from it's original and can be used like regular data. However, in some cases one needs to make sure not to remove or currupt this observable layer on top of the data.

MobX has evolved a lot over the years unlike Redux where most changes and features came from additions to it's ecosystem. MobX used to rely on decorators which was a JavaScript proposal for annotations in classes as they have existed in Java for a long time. However, this technoloy didn't gain adoption in JavaScript and with version 6 of MobX decorators were removed. The newly introduced makeAutoObservable helper makes decorators mostly obsolete anyways.

Creating a Store

In the following example a simple store is set up. The newly introduced makeAutoObservable is used and enables us to avoid assigning all the different types manually for every property. Setting autoBind to true will bind the this context of the store to every action and so avoid a very common beginner mistake and cumbersome workarounds.

import { makeAutoObservable } from 'mobx'
      
class Store {
  counter: { name: string, count: number }

  constructor(counter: { name: string, count: number }) {
    this.counter = counter
    makeAutoObservable(this, {}, { autoBind: true })
  }

  increment(by = 1) {
    this.counter.count = this.counter.count + by
  }

  get double() {
    return this.counter.count * 2
  }
}

const Data = new Store({ name: 'Laps', count: 4 })

First the store is described in the form of a class. This later allows to create as many instances of this store as required. React components only need access to the store instances. Adding types with TypeScript in a store is highly recommended as it's easy to do while avoiding mistakes and making refactoring much easier.

Connecting to React

Where MobX really shines is how it's connected to React components. All that has to be done is to wrap any component accessing data from an observable with observer while any MobX store instances can be imported from anywhere.

import { observer } from 'mobx-react-lite'
import { Data } from './Data'

export const Counter = observer(() => (
  <div>
    <h1>{Data.counter.name}</h1>
    <p>Count: {Data.counter.count}</p>
    <button onClick={Data.increment}>increment</button>
    <button onClick={() => Data.increment(2)}>increment by 2</button>
  </div>
))

Since the data on the Store Data is now augmented and the React component is wrapped with the observer function MobX will track what data is accessed during the render of this component and automatically trigger a rerender if any of the accessed data changes.

Observable / Action / Computed

In cases where the new makeAutoObservable doesn't work as expected you can override some types yourself or assign all of them yourself with makeObservable.

import { observable, action, computed } from 'mobx'

makeAutoObservable(this, {
  counter: observable,
  increment: action,
  double: computed
})

All the data that's subject to change over time is considered an observable. It supports any basic types like strings or numbers as well as complex ones like nested objects or arrays. Observables should only be changed inside actions while state derived from the data should be accessed through computeds which will cache the results and inform observers when their result changes.

Stepping Stone: Scope

If actions don't have access to the store scope this will often lead to hard to debug issues. Therefore, it's important to bind actions to the this scope of the class. This can either be done by using action.bound or enabling autobind when using makeAutoObservable.

import { makeObservable } from 'mobx'

class Store {
  constructor(counter: { name: string, count: number }) {
    makeObservable(this, {
      increment: action.bound
    })
    // or bind automatically.
    makeAutoObservable(this, {}, { autoBind: true })
  }

  increment() { ... }

  // Old approach by binding with arrow function, not recommended anymore.
  increment: () => { ... }
}

What about React's built in state management?

Internal methods like useState should be used when state is only required inside the current component or a set of components that clearly belong together. This approach quickly reaches it's boundaries where state is shared between different components and should be managed with a dedicated tool. The Context API allows to share state between lots of components but is much less flexible and requires more overhead to access.

Future

The biggest drawback apart from the learing curve is the relatively big bundle size at roughly 60 kilobites which is about half the size of react-dom the main package required for react in the browser. Recently, other alternatives using the observable approach with proxies and tight react integration have popped up, but none have gained significant traction yet. valtio is worth mentioning as it works similarly to MobX but is much smaller.

Immutability vs. Mutability

These are the two approaches used to ensure that only the necessary components are rerendered after a change to the state is made.

Immutability used by Redux

For Redux to work properly it's necessary that the whole state structure is recreated after upon every change. Most commonly the ... spread-operator is used to copy the parts of an object that remain unchanged while overriding the new properties.

const state = {
  login: {
    authorized: false
  },
  settings: {
    theme: 'regular'
  }
}

const nextState = {
  ...state,
  login: {
    ...state.login,
    authorized: true
  }
}

In the above example the reference to settings will be copied while for login a new object is created and referenced. Therefore, if only the settings reference is passed to a component it will not trigger a rerender as state.settings === nextState.settings. For login a new object has been created and so there referenced object changes even though inside only the login.authorized variable has changed.

In order for changes to be picked up by all connected components it's necessary to always update all references up to the root with new objects. It seems like this would also lead to a lot of unnecessary rerenders but in practice this approach works fine as the parts to rerender decrease exponentially.

Adding Mutability to useState with immer

Just like Redux's connect, React's built in useState also performs shallow reference comparisons that only work when using immutable objects. immer is a handy tool by the creator of MobX to apply changes made to a mutable object to another object in an immutable way. Like in the example above all object references up to the root will be updated for any nested change. Immer achieves the same by only making the necessary changes on a draft object which will then be turned into a new object with the same references updated as when updating the tree manually.

import produce from 'immer'

const nextState = produce(state, draft => {
    draft.login.authorized = true
})

The useImmer hook can be used to augment the basic useState method in a way that changes can be made without creating a new object and only making the necessary changes before the update.

export const Modal = ({ children }) => {
  const [settings, setSettings] = useImmer({
    open: false,
    color: 'red'
  })
  
  const handleToggle = useCallback(() => setSettings((draft) => {
    draft.open = !draft.open
  }), [])
  
  return <div style={{ display: settings.open ? 'flex' : 'none' }}>{children}</div>
}

The second value usually returned by useState now takes a method the same way as produce from immer does. immer will ensure no changes will go unnoticed and the React component will always rerender on state changes.

Mutability used by MobX

MobX uses JavaScript Proxies to augment the data. Once it's augmented as an observable MobX can track any read or write to the values. Proxies weren't supported by many browsers for a long time. To work around this, MobX used objects with getters and setters to augment the data. This can still be enabled for compatibility with older browsers. Unless it's not supported by the browser MobX will use JavaScript Proxies. Some features are only available with Proxies. However, these limitations shouldn't discourage from using MobX with Internet Explorer 11.

import { configure } from 'mobx'

configure({
  // By default proxies will be used if available.
  // Setting to 'never' can be useful to test compatibility with older browsers.
  useProxies: 'always' | 'never' | 'ifavailable'
})

When a React component connected to a MobX store with observer is rendering MobX will track which values are accessed and will trigger a rerender in case any of those values are later changed by an action.

epic-mobx Plugin

When using MobX often the case arises that one needs to attach a list of stores to a store. Whenever a new element is added to the list a new store needs to be initialized and then added to the list. To alleviate this issue I've created epic-mobx which will manage lists of stores for you.

import { nestable } from 'epic-mobx'

class MyStore {
  parts = nestable([1, 2], Item)
}

In this case Item is another Store class and parts will be filled with two instances of Item like [new Item(1), new Item(2)]. Adding another one is now as easy as calling parts.extend(3). Additionaly, individual items can be removed without a reference to the parent like parts[1].remove().

Usually, in the constructor one has to manually assign all the different top-level variables to the store. epic-mobx also comes with a helper that automatically spreads properties on the store.

import { placeAll } from 'epic-mobx'

class Item {
  constructor({ title, text }) {
    this.title = title
    this.text = text
  }
  // =>
  constructor(data) {
    placeAll(this, data)
  }
}