Best practices for MobX with React

January 03, 2019, 8 min read

This is my list of 8 useful tips and best practices for those that are starting out using MobX with React. It’s not meant as an introduction to MobX and it assumes familiarity with its key concepts.

1. Start by modelling the observable state

Before worrying about anything else, model your observable state—the data that drives the UI. Keep in mind that observable state is what your app needs to function. It’s not meant to be an 1:1 mapping of your database tables/objects. The next step is to identify the necessary actions, followed by any side-effects.

2. Embrace derived state (computed properties)

After modelling the core observable state, think about the derived state. This means thinking how your observers—your UI—are going to consume your observables. The goal it to provide a semantic interface to your observers. Instead of doing low-level operations on your observable state, expose computed properties.

If you find yourself doing things like the following:

// Processing observable arrays to get derived values
store.array.forEach()
store.array.reduce()
store.array.filter()
store.array.find()

// Checking array length
if (store.array.length > 0) {
  ...
}

// Combining values
const fullname = store.firstname + store.lastname

Or, things where essentially the value that you get from the store is not the final value that you need on your UI, then that sounds like a prime candidate for a computed property.

  • If you need to process arrays, your store should do that and provide you with the desired value.
  • If you need to check whether an array contains values, your store should provide an isEmpty property.
  • If you need to combine values (e.g. firstname + lastname to fullname, your store should provide the final—fullname—computed property.

And so forth.

Effectively, what you get from the store should be usable by your UI without any further processing. Don’t worry about performance. MobX is extremely efficient with computed properties.

Computed values can’t be underestimated, as they help you to make your actual modifiable state as small as possible. Besides that they are highly optimized, so use them wherever possible. — MobX

3. Embrace mutability

You might assume that MobX—a state-management library, whose creator went on to develop Immer— is all about immutability and functional-style programming. Your assumption could be, reasonably, backed by your experience with other state libraries—like Redux, which enforces immutability.

MobX is not that kind of library.

MobX creates mutable objects that you can—and should—mutate, directly. It still enables you to create efficient state-machines; it’s just not using immutable structures.

4. Use strict mode

MobX allows you to modify state in two ways; directly or within actions:

// mutate directly
store.valueA = 5;
store.valueB = 10;

// mutate in an action
action() {
  this.valueA = 5;
  this.valueB = 10;
}

When you modify state in actions, you:

  • Improve code readability by:

    • making your intent clear (i.e. “I deliberately want to mutate an observable, not just any object”). This will make your life easier, when revisiting that piece of code in the future.
    • making your app more declarative. Wrapping a bunch of mutations in an action, allows you to give a distinct name—that makes sense to your UI—to that operation.
  • Boost performance by treating the set of changes in the action as one atomic transaction. Only one change notification is fired, after all observables have been modified.
  • Make debugging easier, in combination with the MobX dev-tools.

In strict mode, you are only allowed to mutate observables within actions. MobX will return an error, otherwise.

Enable strict mode with the following code:

import { observable, configure } from 'mobx'

// 'observed' is the recommended strictness mode in non-trivial applications.
configure({ enforceActions: 'observed' })

5. Keep your actions in MobX stores

There are two approaches in choosing where to define actions:

A. Close to components that need them

One approach is to define actions in component files or utility modules (i.e. defining an action in the same file as a React component). This is the simplest way and might allow you to iterate faster, as you develop the app.

Think about what happens when your app grows.

You might end up with a unruly web of actions that are triggered in many places and modify the store.

  • How can you keep track of all the actions?
  • How easily can you refactor them?
  • How can you avoid code repetition when more than one components need to perform the same action?

Keep in mind that when actions are stand-alone functions in separate files, you will also have to pass the store as a parameter.

Strive to keep all actions in one location, close to your stores. This will help you keep track of what’s going on and debug your app, as you’ll only have to look in one place. Define actions in the same file/module as your store; preferably as a store method:

class Person {
  @observable name = ''

  @action setName(name) {
    this.name = name
  }
}

const person = new Person()
person.setName('Kostas')

6. Class vs Object syntax

MobX allows you to create observables in classes and objects. Here are three examples.

  1. Class syntax using @observable, @computed and @action decorators:
class OrderLine {
  @observable price = 0
  @observable amount = 1

  constructor(price) {
    this.price = price
  }

  @computed get total() {
    return this.price * this.amount
  }

  @action setPrice(price) {
    this.price = price
  }
}
  1. Class syntax using the decorate() function:
class OrderLine {
  price = 0
  amount = 1

  constructor(price) {
    this.price = price
  }

  get total() {
    return this.price * this.amount
  }

  setPrice(price) {
    this.price = price
  }
}
decorate(OrderLine, {  price: observable,
  amount: observable,
  total: computed,
  setPrice: action
})
  1. observable.object syntax:
const orderLine = observable.object(
  {
    price: 0,
    amount: 1,
    get total() {
      return this.price * this.amount
    },
    setPrice(price) {
      this.price = price
    }
  },
  {
    setPrice: action
  }
)

Which one should you choose? It doesn’t matter. MobX supports all three methods equally well—you will not miss out on any functionality. It’s only a matter of style and tooling support (e.g. your environment must support decorators to use @observable, etc). So pick what works best for yourself, your team and project. Once you make a choice, stay consistent.

7. Inject store, rather than importing

When you want to use a store in one of your React components, you can just import it (import store from './store.js') and access its properties. That works fine, but there are downsides:

  • It’s not idiomatic MobX. You’re using a tool, but deviate from its recommended way of doing things. This might confusion to team members and future code-maintainers.

  • It’s less declarative. Instead of deliberately injecting a store, you’re importing some module.

  • It’s harder to test.

  • It’s harder to do server-side-rendering.

You might have a reason for not using import; make sure it’s a good reason.

The recommended approach is to use Provider and inject—two components provided by mobx-react.

Provider is a component that can pass stores (or other stuff), using React’s context API, to child components. This is useful if you have things that you don’t want to pass through multiple layers of components explicitly.

inject can be used to pick up those stores. It is a higher order component that takes a list of strings and makes those stores available to the wrapped component.

Provide one or more stores in a parent component:

...
<Provider productStore={ProductStore} uiStore={UiStore}>
  <div>{children}</div>
</Provider>
...

Inject takes provided items from context and makes them available as props. For example, inject('productStore') will take productStore from the context and make it available as this.props.productStore:

// inject in class component
@inject('productStore')
@observer
class Items extends React.Component {
  render() {
    return <span>{this.props.productStore.itemCount}</span>
  }
}

// inject in function component
const Items = inject('productStore')(
  observer(({ productStore }) => <span>{productStore.itemCount}</span>)
)

8. mobx-state-tree (MST)

As you might have realised, MobX is quite flexible and you can use it in a variety of ways. If you are looking for a more opinionated alternative, then see mobx-state-tree.

“Opinionated, transactional, MobX powered state container combining the best features of the immutable and mutable world for an optimal DX”

MST enforces consistency, supports typed observables and is quite different from pure MobX—so there is some learning curve.

Learn more

If you want to learn more about MobX, I recommend the following resources. Start with the official MobX documentation. It is quite good and I think it’s worth going through the whole thing, at least once.

Some highlights:

Keep in mind that mobx-react is a separate package. Read its documentation, here.

Books

Articles