Working with the community as well as companies that are using React & React Native over the past year or so, I get this question a lot: should I use Redux or MobX?

Well, the answer I give to that question varies depending on the client, person, or project I’m working with, but in general I am not biased towards either as they both competently accomplish the goal of state management and do so with elegance and fairly robust communities / documentation.

Now with MobX State Tree, we have another first class contender that should be thrown into the mix. While it is not completely its own separate state management solution (it is very similar to MobX), it is different enough to not categorize it as just “MobX”.

From the docs:

Central in MST (mobx-state-tree) is the concept of a living tree. The tree consists of mutable, but strictly protected objects enriched with runtime type information. In other words; each tree has a shape (type information) and state(data).

How MobX and MobX State Tree are different:

Opinionated — MobX State Tree is very opinionated on how data should be structured and updated, while MobX allows you to structure your data and actions however you would like.

Typed — Includes its own typed API, MobX has no preference

Models — With MobX State Tree, you build trees of models

How MobX and MobX State Tree are the same:

Uses observable data

Mutable

Downsides to using MobX State Tree vs MobX

The type safety of MobX State tree may add some runtime overhead. (Read more about when not to use MobX State Tree here).

Also see “How does MobX State Tree compare to Redux” To use decorators with MobX State Tree and React Native, you must also install this babel plugin and configure your .babelrc. I have a demo of how to do this in this blog post.

Creating a Store in MobX State Tree vs MobX

Let’s take a quick look at a very basic store using MobX State Tree. We will modify and work with this store to add functionality as this post goes on.

// BookStore.js import { types } from "mobx-state-tree" // A const Book = types.model('Book', { // B

title: types.string,

author: types.string,

read: false

}) const BookStore = types.model('Books', { // C

books: types.array(Book)

})

.create({ // D

books: []

}) export default BookStore

A. We import the types namespace from mobx-state-tree . This is what you use to define the types in your store.

B. We then create a Book type with title , author , and read properties.

C. Using the Book type, we then create a BookStore type using the Book type to create .

D. We call .create on the BookStore, passing in an empty array of books.

The following store would be the same if we were using MobX:

import { observable } from 'mobx' class BookStore {

@observable

books = []

} export default new BookStore()

To use either of these stores, you can just import the store into your component, so this part does not change between the two libraries:

// SomeComponent.js import BookStore from './pathtoBookStore' const { books } = BookStore // somewhere in your UI:

books.map(book => /* do something with book */)

There are three types of types: primitive, complex, and utility.

Primitive types:

types.string

types.number

types.boolean

types.Date

Complex types:

types.model(properties)

types.array(type)

types.map(type)

Utility Types

Adding functionality to our store

Actions

The next obvious step would be to add an addBook function that would allow us to add books to our store. In MobX State Tree, we need to do that using actions.

From the docs:

By default, nodes can only be modified by one of their actions, or by actions higher up in the tree. Actions can be defined by returning an object from the action initializer function that was passed to actions .

Let’s update the BookStore to have the addBook action:

const BookStore = types.model({

books: types.array(Book)

})

.actions(self => ({ // A

addBook(book) {

self.books.push(book)

}

}))

.create({

books: []

})

A. We chain an action initializer function, which gives us a reference to self , and returns an object. The initializer function is executed for each instance, so that self is always bound to the current instance. We then reference self.books and call .push , passing in the book we would like to add to the books array!

What if we wanted to add a function that just affected the book within the books array? The easiest way would be to add an action to the Book model.

Here, we will add a function that toggles the books’ read boolean property:

const Book = types.model('Book', {

title: types.string,

author: types.string,

read: false

}).actions(self => ({

toggleRead() {

self.read = !self.read

}

}))

The beauty of this is that we do not need to do any extra work to find the item in which we would like to perform this action on. For instance in redux, we may have to search for this item in the array, make the manipulation, then reconstruct and return the array.

Here, because we have access to the item as self , we can just manipulate the property directly, which is really nice.

We can now work with this function as if it were a property of the book:

books.map(book => (

<Text onPress={book.toggleRead}>

Read: {book.read ? 'Yes' : 'No'}

</Text>

))

There are also some really useful helper functions, like destroy :

import { types, destroy } from "mobx-state-tree" // A const BookStore = types.model({

books: types.array(Book)

})

.actions(self => ({

remove(book) { // B

destroy(book)

}

}))

.create({

books: []

})

A. We import destroy from mobx-state-tree

B. We create a remove action, calling destroy and passing in the item.

When using destroy , MST will remove it actually from the books collection so that you don’t have to find it yourself.

There are also lifecycle hooks for models, like beforeDestroy , that can be hooked into for more granular control.

Computed Properties

Introducing views

From the docs:

In MobX, anything that can be derived from your state is called a “view” or a “derivation”. …Views come in two flavors. Views with arguments and views without arguments. The latter are called computed values, based on the computed concept in MobX.

Computed values are defined using getter functions.

Let’s add a couple of views to our BookStore model to demonstrate this:

const BookStore = types.model({

books: types.array(Book)

})

.views(self => ({ // A

get readBooks() { // B

return self.books.filter(book => book.read)

},

booksByAuthor(author) { // C

return self.books.filter(book => book.author === author)

}

}))

.actions(// previously defined actions)

A. We chain the .views initializer function onto our Model, which gives us a reference to self , returning an object with the views we would like available.

B. Our first view is a computed value readBooks , that returns only books that have been read

C. We also create a booksByAuthor view which takes an argument, returning only books written by the author passed in.

Conclusion

In general, the API is pretty large and all of it cannot be covered here in this post, but overall I can say that almost anything that you need can be accomplished with this library when it comes to state management.

If you like MobX but are looking for something more opinionated, check out MobX State Tree.