Skip to content

MobX state management in React

Manage state outside of the view layer with ease

MobX is a simple, scalable, boilerplate-free state management solution. It allows you to manage application state outside of any UI framework, making the code decoupled, portable and, above all, easy to test.

It implements observable values, which are essentially using the publish/subscribe pattern. They differ in that the subscriptions are handled automatically. The engine tracks where you used the observables and only re-executes the side effects, like a render, if they change.

Usage

Here is a simple counter app. We will use a class to organise our state. This is the most flexible way to use MobX. You are not required to do so, but we will explain that later.

You can find this example and more advanced ones in the demo repository .

js
import { makeObservable, observable, computed, action } from 'mobx'
import { observer } from 'mobx-react-lite' 

class Store {
  count = 0

  constructor() {
    makeObservable(this, {
      count: observable,
      isNegative: computed,
      add: action,
      subtract: action
    })
  }

  get isNegative() {
    return this.count < 0 ? 'Yes' : 'No'
  }

  add() {
    this.count += 1
  }

  subtract() {
    this.count -= 1
  }
}

const store = new Store()

// observer lets MobX track this component's dependencies
const App = observer(() => (
  <>
    Count: {store.count}

    Is negative? {store.isNegative}

    <button onClick={store.add}>Add</button>
    <button onClick={store.subtract}>Subtract</button>
  </>
)

Note the separation of view and data/logic layer. With MobX, virtually all your components will be “dumb". Separating these concerns leads to code that is easy to reason about, reuse, refactor and test.

You can also see that we didn’t need to explicitly specify the component’s dependencies from the store. MobX tracks them automatically and builds a dependency graph that captures all relations between state and output. This guarantees that your React components render only when strictly needed. This is a very powerful feature that spares you from using error-prone techniques such as memoization or selectors. Performance concerns are a thing of the past.

Observables

Observables represent the reactive state of your application. They can be simple scalars, objects, arrays, maps, sets or even custom defined ones . Any change to this data triggers reactions, which in turn trigger side-effects, such as a React render. The reactivity system will make sure that only relevant dependencies are updated.

To make a value observable, we need to mark it as such so that MobX will track it. There are multiple ways to do that, as shown below.

js
import { observable } from 'mobx'

class Store {
  counter = observable(0)
}

// or
class StoreOther {
  counter = 0

  constructor() {
    makeObservable(this, {
       counter: observable,
    })
  }
}

// or if you fancy decorators
class StoreDecorators {
  @observable counter = 0
}

Computed values

Computed values are used to derive information from other observables. Conceptually, they are similar to formulas in a spreadsheet. They differ from other libraries in that they are automatically memoized and hence update only when their dependencies change. They help us store a minimum amount of state and keep the logic decoupled from the view layer. To create a computed value, we use a get function and mark it with computed .

js
import { computed, makeObservable } from 'mobx' 

class ProductsStore {
  products = []
  query = ''
  totalCount = 100

  constructor() {
    // mark getters as computed views
    makeObservable(this, {
      ids: computed,
      count: computed,
      hasMore: computed,
    })
  }

  get ids() {
    return this.products.map(product => product.id)
  }

  get count() {
    return this.ids.length
  }

  get hasMore() {
    return this.count < this.totalCount
  }
  
  // again, if you fancy decorators you can do it like this
  @computed get matchingProducts() {
    return this.products.filter(product => product.name.includes(this.query)
  }
}

Actions

Actions are functions that modify the state. Any code can do so, but actions have performance optimisations and keep the code decoupled. They run in transactions, meaning no observers will be updated until the outermost action has finished. This will ensure that intermediate changes to state do not trigger unnecessary re-renders. Furthermore, they can be asynchronous without any special work.

text
import { observable, action, makeObservable } from 'mobx' 

class ProductsStore {
  products = []
  fetching = false

  constructor() {
    makeObservable(this, {
      products: observable,
      fetching: observable,
      fetchProducts: action,
      setProducts: action,
      setFetching: action
    })
  }

  async fetchProducts() {
    this.setFetching(true)
  
    try {
      const res = await fetchProductsSomehow()
      this.setProducts(products)
    } catch (error) {
      console.log('Failed to fetch products', error)      
    } finally {
      this.setFetching(false)
    }
  }

  setProducts(products) {
    this.products.replace(res.data)
  }

  setFetching(fetching) {
    this.fetching = fetching
  }
}

Note that having separate actions to modify state in an async action is only required with the default enforceActions option. It is useful when starting out to enforce separation of concerns, but it can be turned off to provide more flexibility once you are used to MobX.

makeAutoObservable

Now that we understand the core concepts, we can reduce the necessary boilerplate to a single function call using makeAutoObservable() . This lets MobX figure out the annotations behind the scenes automatically. Using it, the store to our counter app is reduced to this:

js
class Store {
  count = 0

  constructor() {
    makeAutoObservable(this)
  }

  get isNegative() {
    return this.count < 0 ? 'Yes' : 'No'
  }

  add() {
    this.count += 1
  }

  subtract() {
    this.count -= 1
  }
}

Reactions

Reactions are what trigger the reactivity in the system. This is where the magic of MobX comes together. A reaction implicitly tracks all the observables being used and then re-executes the side effect whenever the dependent observables change.

They come in multiple flavours — autorun , when , reaction and observer (in React).

Observer

The most used reaction is the observer , which tracks observables in a React component. Internally, it will figure out which observables are being used and then re-render the component only when strictly necessary. As mentioned earlier, this is one of the most powerful features of MobX. It makes your app highly performant without any effort on your part.

Autorun, reaction and when

Other reactions should be used sparingly — only when you want to produce a side-effect and there is no direct relation between cause and effect. They all work the same but differ in the level of granularity you can specify in the dependencies. Autorun is the simplest one and will figure out the dependencies automatically.

js
autorun(() => console.log('Products count changed', this.products.length))

Reaction lets you specify the dependencies with a predicate.

js
reaction(() => this.products.length, () => console.log('Products count changed'))

When is similar to Reaction but only runs when the predicate returns true .

js
when(() => this.products.length > 10, () => console.log('Has more than 10 products'))

Remember that computed is a reaction too. The difference is that it produces a value , whereas other reactions produce a side-effect . It is therefore called a derivation .

Usage without classes

MobX can be easily used without classes. There are some advantages to using classes such as inheritance, but you may use the following syntax:

js
const store = makeAutoObservable({
  // observable
  products: [],
  
  // computed
  get count() {
    return this.products.length
  }  


  // action
  addProduct(product) {
    store.products.push(product)
  }
})

// reaction can be defined anywhere
reaction(() => store.count, () => console.log('count changed')

// actions too (without strict mode)
const clearProducts = () => store.products.clear()

Going further

MobX works outside of any UI framework, so it is not bound by their programming model limitations. The advised way to use MobX is to use classes and OOP, which unlocks incredible productivity and easy code reuse. The example here can be reconstructed using other state solutions, but MobX’s minimalistic boilerplate makes it especially easy..

A common issue with larger, dashboard-style applications is managing collections of data. It usually involves some fetching, filtering, pagination and sorting. Writing the same code over and over for each page can be cumbersome and tedious. With MobX, it’s straightforward to create a reusable and configurable module that handles all these common tasks. The code for this is too long to include here, but you can check it out in the demo repository .

From this comprehensive review of MobX, we can see that it has no boilerplate — just some observable data, computed views, actions and reactions . The reactivity is handled by the engine, and it just works . Some skeptics describe it as too “magical”, but once you start using it you will see that this is not the case. You can learn more about the inner workings in the official documentation here: Understanding reactivity in MobX .

The simple API also means that MobX is un-opinionated about how you actually structure your stores. This can be a bit of a double-edged sword. For larger projects, proper engineering practices and planning need to be applied. Interestingly, most users end up converging to a similar structure, also described in the official documentation - Defining data stores in MobX .

Opinionated projects have been built on top of the MobX engine. The most notable one is mobx-state-tree . It combines the best of both worlds — MobX reactivity with Redux-like tree structure, transactions and time travel. Mobx-keystone is similar. However, in both cases you lose some of the flexibility of pure MobX.

Insight, imagination and expertly engineered solutions to accelerate and sustain progress.

Contact