2019-10-09 by Alon Bar David

Why you should use MobX

A comparison of state management solutions for react

mobx on superman

I've used various state management libraries over the years (with react and not) but for the past few years MobX is my go to solution for everything - from simple toy apps to complicated apps built by multiple teams, I'm even using it in frameworks that aren't react (Vue.js).

Why am I using it so often? I'll try and answer that by listing MobX's advantages and comparing each advantage to what I think are failings of alternative solutions (Since this is geared more towards react users, I'll compare it to redux and state management using context)

This article assumes you are familiar with MobX and how it works if you aren't, you should read the MobX documentation.

Advantages of MobX

MobX has minimal boilerplate

Redux is notorious for it's boilerplate. Here's a simple api call recipe in redux, something every app has in one form or another:

// in actions.js
// these are action creators
export const  requestResource= (resourceId)=>({
  type: 'REQUEST_RESOURCE',
  resourceId
})
export const requestResourceSuccess = (resourceId,payload)=>({
  type: 'REQUEST_RESOURCE_SUCCESS',
  resourceId,
  payload
})
export const requestResourceFailure = (resourceId,error)=>({
  type: 'REQUEST_RESOURCE_FAILURE',
  resourceId,
  error
})
export const fetchResource = resourceId => dispatch =>{
  dispatch(requestResource(resourceId));
  return fetch(`url/resource/${resourceId}`)
    .then(response=>response.json())
    .then(json=>dispatch(requestResourceSuccess(resourceId,json)))
    .catch(e=>dispatch(requestResourceFailure(resourceId,e)))
}

// in reducers.js
export default (state = {}, action) => {
  switch (action.type) {
    case 'REQUEST_RESOURCE':
        return {
            resources:{
                ...state.resources,
                [action.resourceId]: {loading:true}
            }
        }
    case 'REQUEST_RESOURCE_SUCCESS':
        return {
            resources:{
                ...state.resources,
                [action.resourceId]: {loading:false,resource:action.payload}
            }
        }
    case 'REQUEST_RESOURCE_FAILURE':
        return {
            resources:{
                ...state,
                [action.resourceId]: {loading:false,error:action.error}
            }
        }
    default:
      return state
  }
}

That's a lot of code to do something very simple, and that's before we've added the minimal libraries to make redux usable in non-trivial applications - redux-thunk/redux-saga , reselect (for momoization) and normalizr (for normalization).

In contrast a MobX store that does the same would be written like this:

class ResourceStore {
  @observable
  resources = {};
 
  @action("REQUEST_RESOURCE")
  requestResource(id) {
    this.resources[id] = {loading:true};
    return fetch(`url/resource/${id}`)
        .then(response=>response.json())
        .then(action("REQUEST_RESOURCE_SUCCESS",
              resource=>{this.resources[id] = {loading:false,resource};}))
        .catch(action("REQUEST_RESOURCE_FAILURE",
              error=>{this.resources[id] = {loading:false,error};}))
  }
}
    

And for those who are wondering that is exactly the same functionality as the previous redux example. Beyond being more concise it's also much clearer and direct, there's nowhere for extra functionality to creep up at you.

Unlike the redux example it automatically memoizes and there's no need for thunk or saga to work in a real app. (It doesn't normalizes though, that takes a bit more work to do)

There are various attempts to reduce the boilerplate - from redux itself, such as the actionCreatorCreator (I cringe just hearing that name) to full-fledged conventional libraries like ducks. Unfortunately they either didn't live up to their claims or haven't gained much popularity.

The extreme amount of boilerplate code isn't limited to the logic layer part of creating redux reducers. It's also added to the consumer part - react components:

//resource-view.js
function ResourceView(props) {
  const {loading,resource,error} = props;
  return loading? <loader/>:<ResourceItem resource={resource}/>
}

const mapStateToProps = (state,ownProps) => ({
    ...state.resources[ownProps.resourceId]
})
export default connect(mapStateToProps)(ResourceView)

// resource-list.js
function ResourceList(props) {
  const resourceIds = [1,2,3,4,5,6,7,8]; //Just for example sake, this would usually be taken from the store as well
  const {requestResource,resources} = props;
  return <div>
    {resourceIds.map(id=><div key={id}>
      {resources[id]?<ResourceView resourceId={id} />:
      <button onClick={()=>requestResource(id)}>load</button>}
    </div>)}
  </div>
}
const mapStateToProps = (state,ownProps) => ({
    resources:state.resources
})
const mapDispatchToProps = (dispatch,ownProps) =>({
    requestResource:()=>requestResource(ownProps.resourceId)(dispatch)
})
export default connect(mapStateToProps,mapDispatchToProps)(ResourceView)

Another option for state management in React is to use the Context api.

Let's make the same api example with Context and Hooks:

const ResourceContext = React.createContext();
function useResource(){
  const [resource,setResource] = React.useState({});
  return {
    resource,
    requestResource(){
      setResource({loading:true});
      return fetch(`url/resource/${resourceId}`)
        .then(response=>response.json())
        .then(json=>setResource({loading:false,resource:json}))
        .catch(error=>setResource({loading:false,error}))
    }
  }
}

function resourceProvider(){
  const value = useResource();
  return <ResourceContext.Provider value={value} >
    {children}
  </ResourceContext.Provider>
}

That's not too different from the MobX example - we only add createContext and Context.Provider. However we need to use Context.Provider and useContext every time we want state that needs to be used separately.
The more your application grows and state moves up the tree the more this becomes a common occurrence until your top level component looks something like this:

  function App(){
    return <PostsContext.Provider>
            <NotificationsContext.Provider>
              <AuthenticationContext.Provider>
                ... ad nauseum 
              </AuthenticationContext.Provider>
            </NotificationsContext.Provider>
          </PostsContext.Provider>      
  }

If you follow Kent C. Dodds' guidelines on how to build state, you'll probably say that this shouldn't happen because state should only be as close to the usage as possible and not all of it should stay at the top.

Unfortunately I find that a lot of common use cases require state to be hoisted to the top of the app - anything that needs to be shown in 2 sections of the app (such as in the main window and the sidebar, or navbar), for example: notifications/messages , authentication (and my user info), showing if a feed/group has more items to show, number of online users and a lot more.

Since you often bundle state and actions together with MobX it also lessons the problem of prop drilling - it's much easier to send a single prop down several level than multiple ones, which makes your lower level components simpler.

MobX always renders the minimal required components

Out of the box, and with little effort, MobX will rerender only components whose render tree will actually change when you update a specific part of the store. That means that MobX will never render a component unless the props it depended on changed; it'll never render the parent of the component that actually changes or it's siblings .

To get similar functionality with redux you need to use the difficult and very manual process of memoization.
Let's take our resource api example from the previous point.

//resource-view.js
function ResourceView(props) {
  const {loading,resource,error} = props;
  return loading? <loader/>:<ResourceItem resource={resource}/>
}

const mapStateToProps = (state,ownProps) => ({
    ...state.resources[ownProps.resourceId]
})
export default connect(mapStateToProps)(ResourceView)

// resource-list.js
function ResourceList(props) {
  const resourceIds = [1,2,3,4,5,6,7,8]; //Just for example sake, this would usually be taken from the store as well
  const {requestResource,resources} = props;
  return <div>
    {resourceIds.map(id=><div key={id}>
      {resources[id]?<ResourceView resourceId={id} />:
      <button onClick={()=>requestResource(id)}>load</button>}
    </div>)}
  </div>
}
const mapStateToProps = (state,ownProps) => ({
    resources:state.resources
})
const mapDispatchToProps = (dispatch,ownProps) =>({
    requestResource:()=>requestResource(ownProps.resourceId)(dispatch)
})
export default connect(mapStateToProps,mapDispatchToProps)(ResourceView)

If we use redux naively all ResourceView components will rerender every time any resource in the store changes.

To change it to be more performant we need to use memoization.
Let's change the code to use the reselect library:

// resource-view.js

const resourceById = createSelector([
    (state,props)=>state.resources[props.resourceId]
],(resource)=>resource)
const mapStateToProps = (state,ownProps) => ({
    ...resourceById(state,ownProps)
})

This is more boilerplate, but beyond that it is hard to work with it - you need to manually consider what permutations of state need to rerender your component and take them all into account.
If your components are slightly more complex it also becomes a nightmare to maintain and extend (usually this will be the case for top level components such as pages or navigations)

Using context for state management suffers similar problems, if you wanted to only render the ResourceView when the actual resource changes, you'll have to create a Context for each resourceId (or alternatively restructure your app in some way).

Even if you did all that though there are still edge-case that only MobX can deal with.
Consider a situation where you want to count the second since your fetch was called inside a resource property that is updated while something is loading and show how long the request took when it's done.

const ResourceContext = React.createContext();
function useResource(){
  const [resource,setResource] = React.useState({});
  return {
    resource,
    requestResource(){
      const interval = setInterval(()=>({
        // this is a contrived example for brevity, I know it's not
        // how you'll do it and that it won't work since resource is
        // always the original value
        setResource({...resource,seconds:resource.time + 1 })
      }),1000)
      setResource({loading:true,seconds:0});
      return fetch(`url/resource/${resourceId}`)
        .then(response=>response.json())
        .then(json=>setResource({...resource,loading:false,resource:json}))
        .catch(error=>setResource({loading:false,error}))
        .finally(()=>clearInterval(interval))
    }
  }
}

//resource-view.js
function ResourceView(props) {
  return props.loading? <Loader/>:<ResourceItem resource={props.resource}/>
}

Now every second ResourceView rerenders even though we are only showing the <Loader/> component and nothing actually changes.
Reselect and Context are simply unable to determine that as long as the loading property is true no other property of resource is being used.

Mobx however will by default not rerender ResourceView until the loading property is changed - because MobX only renders based on what properties were used in the last render.

Note: this characteristic of MobX depends on referencing the observable at the bottom most components and for every component to use observer (both are the natural way to use MobX in React).
If you reference an observable value at the top of your app and send that value as a prop throughout the render tree, everything will render on every change. (so don't do that)

MobX doesn't require you to change your code

Using MobX looks just like normal javascript, it doesn't require you to change your code or architecture to support it (unlike Redux and to a lesser extent Context).

In fact it's such an invisible abstraction that in many cases if you take out all of the MobX code - the @observable, @computed, @action and observer decorators, your code will work exactly the same (though it'll have some performance issues).

In it's core MobX isn't a state management solution at all, it's a meta-programming approach to increase performance - as such it is utterly unopinionated about how you structure your code.
The state management part is what you build with it.
Some will consider that a drawback, but I like my frameworks and libraries to be as unopinionated as possible so I can structure my code the way I think is best.

MobX is easily composable

One of the biggest problems with redux, at least for me, is that because of it's global nature it's hard to compose it (and composeReducers is definitely not the kind of composing I mean).

The main reason why redux has problems with composability is that the path within the store in redux matters. With the standard way of using redux you can't use the same component for a reducer module that sits under authentication/user/current and groups/user/manager even if the store itself is exactly the same.
If you use the connect function you'll need to use different HOCs for the same component or create a component that accepts the path of the store as a property.

That's why normalization is such a big factor in Redux - if you always put all of the same objects in the same location you don't get this problem.
But this means that every reducer that needs to display a shared object needs access to the pool of shared resources - for instance every api call for a store that loads a user needs access to the shared user store.

This breaks the ability of modules to be self-contained but it isn't too bad for most apps, the problem really lies when starting to use third-party components.
Since third-party components and modules can't predict how your store will look they can't take benefit of your store without some terrible shenanigans. That's one of the reasons why redux doesn't have a lot of popular user modules/libraries (like simple REST consumers for instance) but does have a lot of middlewares.

For a great example why redux makes composable utility libraries hard look at redux-forms - whenever you create or use an instance of redux-form you must give it the path in the store (and it has to be top level in the reducer as well)

Logic shouldn't be part of the view layer

I started doing web UI before the proliferation of the single-page-application and just like now there were a lot of competing ideas and frameworks. The one thing they all agreed on though is that your view layer should never contain logic (whether you called the objects in that layer controllers, DAOs or fat models).

Perhaps I'm old fashioned but I still can't bear to see logic inside the view layers - and in React that means inside a component.

There are good reason why you shouldn't put logic inside your view layer:

  • It makes it harder to refactor - if your components know your store's structure (which is what happens with connect in redux) than any refactor that changes the structure requires you to change the view layer. It also ties your app's logic to how your view is structured - which is a problem if you have different type of views to the same model.

  • It's harder to maintain and reason about an app's flow if the view is in charge of it - If your components call apis then to know if an action would result in an api call you have to follow the lifecycle and rendering of your render tree. When your logic is detached from the view the result of actions are much more deterministic and aren't tied to what page you are viewing or what components are currently showing

  • It makes testing slower and more complex - If your view is responsible for calling apis and performing business logic it means that you need to initiate the view (in this case React's rendering) in order to test an app's flow. If your logic isn't connected to the view than testing it is usually as simple as testing a regular javascript function.

  • Cross-cutting concerns are harder to use - things like logging, caching and error handling are much harder to do when they are split between multiple components.

These problems are also relevant if you use react-mobx's inject to extract state from store inside components.

MobX isn't limited to global state

Not everything should sit in the logic layer. Things that are local to a component or that aren't shared between far away components shouldn't be kept in side the component's state.
Forms are an example of something that should probably be local to your component (unless it's persisted between sessions).

The same benefits of using MobX for a global state, namely performance and lack of boilerplate, can be achieved when using MobX inside your component.

Let's take a simple form example using hooks:

class Form {
 @observable
 email = ""
 @observable
 password = ""
 
 @computed
 get emailError(){
   if (this.email && this.email.length >0 &&
       this.email.indexOf("@") >= 0){
       return "email must contain @ character"
   }
   return null
 }
 @computed
 get passwordError(){
   if (this.password && this.password.length >0 &&
      this.password.length < 8){
      return "password must have 8 characters"
   }
   return null 
 }
 @computed
 get valid(){
   return this.email && !this.emailError &&
     this.password && !this.passwordError
 }
 
 @action
 setEmail(event){
   this.email = event.target.value;
 }
 
 @action
 setPassword(event){
   this.password=event.target.value;
 }

}

return function FormView(props){
 const {submitForm} = props;
 const form = useMemo(()=>new Form(),[true]);
 return <form onSubmit={()=>form.valid && submitForm(form)}>
   <input name="email" value={form.email} 
     onChange={form.setEmail} />
   {form.emailError && <div>{form.emailError}</div>}
   <input name="password" value={form.password} 
     onChange={form.setPassword} type="password"/>
   {form.passwordError && <div>{form.passwordError}</div>}
 </form>
}

Drawbacks

MobX doesn't work well with immutability

Relying extensively on mutation to figure out what needs to be recalculated, MobX doesn't work well with immutability.
It's not that immutability doesn't work, simply that if you use immutability often you'll lose the performance benefits that MobX provides, and with that there's not much to gain by using it.

MobX is harder to track and debug

Since MobX uses "magic" , A.K.A. meta-programming, to keep track of what observables are being used and subscribed to it hides a lot of it's implementation from the normal debugging and programming tools.

Debugging becomes a bit of an issue when you want to realize what caused something to recalculate/rerender and what will rerender if you change something. That's the most powerful feature of redux - all of the boilerplate is for the benefit of keeping the app flow understandable and maintainable.
With very large apps and with multiple developers working on the same app MobX can grow hard to maintain and strict conventions need to be develop to keep the state and app flow manageable.

Redux's ability to rewind time and keeping track of state changes is also a very strong tool for maintaining and debugging code, which MobX can't really do (if you use actions and strict mode you'll get an approximation of it, which is good, but not close enough).

The MobX state tree attempts to fix some of these pain points, but at the cost of being very opinionated and losing a lot of the flexibility to draws people into using MobX.