Why you should use MobX State Tree in your next React project
Tags: javascript, react, reactnative, mobx
Published: 2020-05-12 08:48:40

At [Lloyds](http://lloyds.design/) we write a lot of React and React Native apps. All apps require some state management, so we naturally had to choose a state management library to use in our projects. After some consideration and testing, some back and forth with Redux, MobX and some other solutions, we decided to try MobX State Tree. We loved the features, simplicity and developer experience so much! We just had to share it, so we decided to write this article.

### MobX

MobX is awesome! It’s simple, performant and easy to learn.

We define our observable state and simply access that state in components. Whenever our state changes, our component re-renders automatically. 

We can define the simplest counter app like this:

```jsx
    const state = observable({
      count: 0
    });
    
    const CountView = observer(() => {
      return (
        <View>
          <Text>{state.count}</Text>
          <Button onPress={() => state.count++} title="CLICK ME" />
        </View>
      );
    });
```

We use the state just like a regular JavaScript object. MobX tracks the properties we access on the state and re-renders our component whenever those properties change. Notice that we marked the state to be observable and wrapped our component in the `observer` HOC that enables MobX to track properties and re-render our component.

Let’s consider an example that’s a bit more complex. We can create a simple to-do app.

```javascript
    const state = observable({
      todoList: ["Buy milk"]
    });
    const actions = {
      addTodo(todo) {
        state.todoList.push(todo);
      },
      removeTodo(todo) {
        state.todoList.remove(todo);
      }
    };
    const TodoApp = observer(() => {
      const [todo, setTodo] = useState("");
      return (
        <View style={S.container}>
          <TextInput value={todo} onChangeText={setTodo} placeholder="I have to..." />
          <Button
            title="ADD"
            onPress={() => {
              actions.addTodo(todo);
              setTodo("");
            }}
          />
    
          <Text>Todos:</Text>
          {state.todoList.map(todo => (
            <View style={S.row}>
              <Text>{todo}</Text>
              <Button title="X" onPress={() => actions.removeTodo(todo)} />
            </View>
          ))}
        </View>
      );
    });
```

Yes, MobX really is great, but as the application grows, so does the application state so naturally you start breaking stuff up into multiple stores and communication between different parts of the app starts getting complicated.

### MobX State Tree

MobX gives us a lot out of the box, but we can get a whole lot more by using MobX State Tree. MST is a state management library built on top of MobX. It’s stricter than MobX, but we get some additional features when we use it. We keep our state in a tree of models and we can freely navigate up and down the tree structure.

Additionally, we get some nice features that make for a delightful developer experience.

#### Data Validation

Notice that, in the pure MobX implementation, we have the action `addTodo` that accepts one parameter and pushes that in the `todoList`.

```javascript
      addTodo(todo) {
        state.todoList.push(todo);
      },
```

We always pass a string as the parameter to `addTodo`, but we could easily pass some invalid data to `addTodo` by accident. In a large app, it’s not uncommon to deal with the same state from multiple parts of the app, and it’s easy to mistake `addTodo(todo)` with, for example, `addTodo({ todo })`.

If we push an object in the `todoList` array, the app won't throw an error in our data layer. If we’re lucky, we’ll get an error from the view layer when React tries to render an object as a child of a text component, but we’ll see that error only when we actually render the todos. 

If we’re not lucky, the buggy todo will stay in the array, waiting to crash some app for some unsuspecting user.

It would be nice if we could get an error as soon as we try pushing invalid data in the `todoList` array. That’s where MST data validation comes in.

Let’s rewrite the above todo app example with MST and see how it works. 

```javascript
    const Store = types
      .model("Store", {
        todoList: types.array(types.string)
      })
      .actions(self => {
        return {
          addTodo(todo) {
            self.todoList.push(todo);
          },
          removeTodo(todo) {
            self.todoList.remove(todo);
          }
        };
      });
    
    const store = Store.create({
      todoList: ["Buy milk"]
    });
    
    const TodoApp = observer(() => {
      const [todo, setTodo] = useState("");
      return (
        <View>
          <TextInput
            value={todo}
            onChangeText={setTodo}
            placeholder="I have to..."
          />
          <Button
            title="ADD"
            onPress={() => {
              store.addTodo(todo);
              setTodo("");
            }}
          />
          <Text>Todos:</Text>
          {store.todoList.map(todo => (
            <View style={S.row}>
              <Text>{todo}</Text>
              <Button title="X" onPress={() => store.removeTodo(todo)} />
            </View>
          ))}
        </View>
      );
    });
```

First thing we do is define the data model. You can think of models as schemes that define the shape and types of your data. Additionally, we can add actions that modify the data. That way we keep the data and the actions that modify that data in a single location. This concept is known as [encapsulation](https://www.google.com/search?q=encapsulation).

In our example we create a Store model where we keep the array of todos and actions for adding and removing todos from the array. We expect the todos themselves to be strings so we define them as such using  `types.array(types.string)`.

```javascript
    const Store = types
      .model("Store", {
        todoList: types.array(types.string)
      })
      .actions(self => {
        return {
          addTodo(todo) {
            self.todoList.push(todo);
          },
          removeTodo(todo) {
            self.todoList.remove(todo);
          }
        };
      });
```

Then we create an instance of the model using `Store.create()` and pass the initial state as the first argument.

When instantiating models, MST will validate the data and throw developer friendly errors if the data doesn’t match the defined schema. For example, if we tried passing `{ todoList: [ {"todo":"Buy Milk"} ] }` as the initial state, we would get the following error.

```
    [mobx-state-tree] Error while converting `{"todoList":[{"todo":"Buy Milk"}]}` to `Store`:
    
    at path "/todoList/0" snapshot `{"todo":"Buy Milk"}` is not assignable to type: `string` (Value is not a string).
```

This helps us catch and fix bugs early and follows the principles of [defensive programming](https://www.google.com/search?q=defensive+programming).

CodeSandbox:

https://codesandbox.io/s/mst-todo-app-dhj3r

Data validation is not the only great feature MST has to offer. Another cool feature are references.

#### References

References offer a way to - you guessed it - reference model instances in a safe and simple way. In order to use references we first have to define identifiers on our models. So let’s extend our todo app to see how this works.

First we’ll create a Todo model and add an autogenerated identifier prop.

```javascript
    const Todo = types
      .model("Todo", {
        id: types.optional(types.identifier, () => Math.random().toString()),
        text: types.string
      })  
```

We generate a random id every time a new todo is created.
 
Next we’ll modify the Store model by changing the `todoList` prop to be an array of our newly defined Todo models.

We’ll also add the `selectedTodo` prop and set its type to be a `safeReference` to a `Todo`, and add an action to set the selected todo.

```javascript
    const Store = types
      .model("Store", {
        todoList: types.array(Todo),
        selectedTodo: types.safeReference(Todo)
      })
      .actions(self => {
        return {
          /* ... */
          selectTodo(todo) {
            self.selectedTodo = todo.id;
          }
        };
      });
```

So, the way references work is like this - when setting a reference, we provide an identifier of an existing model instance. On the other side, when we access the reference, MST will automatically resolve the model instance and return it. If we delete the selected todo it will get removed from the tree and the reference will be set to undefined.

We change the component to highlight the selected todo with green background.

```javascript
          /* ... */
          <Text>Todos:</Text>
          {state.todoList.map(todo => {
            const selected = todo === state.selectedTodo;
            const backgroundColor = selected ? "#8f8" : "#fff";
            return (
              <TouchableOpacity
                style={[S.todoWrap, { backgroundColor }]}
                onPress={() => state.selectTodo(todo)}
              >
                <Text style={S.todoText}>{todo.text}</Text>
                <Button title="X" onPress={() => state.removeTodo(todo)} />
              </TouchableOpacity>
            );
          })}
          /* ... */ 
```
 
Note that `state.selectedTodo` is the actual todo instance (with `id`  and  `text` properties and all actions defined on the Todo model).

CodeSandbox:

https://codesandbox.io/s/mst-todo-app-with-references-1xel4


#### Async Actions

When using MST it’s recommended to write async actions using the `flow` helper and generator functions. Generators can be a bit overwhelming for new users, but using generators in MST is really simpler than it seems. Here’s how you can do a simple API call.

```javascript
    .actions(self => {
      return {
        getTodos: flow(function*() {
          self.loading = true;
          const response = yield getEnv(self).http.get("/todos");
          self.loading = false;
          self.todoList = response.data;
        })
      }
    })
```

Flows with generators are similar to async/await. You just replace `await`  with `yield` and `async function` with `function *`. This enables MST to batch UI updates. For example, if we were to use async/await to fetch the todos, the UI would be updated twice - once for `self.loading = false` and a second time for `self.todoList = response.data`. When using generators, MST can wait until the async action is over or yields and only then re-render the UI which improves app performance.

If you’re interested in learning more, there’s a lot more features in MST described on the [official site](https://mobx-state-tree.js.org/).


### Architecture 

So far we’ve introduced some core MST features that we love. Even though all the features we talked about are great, it still took time until we came up with a way to structure the stores and define a directory structure that we use today. 

We strive to reduce data redundancy (avoid same data defined in multiple places). We want to have a single source of truth at all times. The next section of this article explains how we used MST to achieve this goal. 

#### Data Normalization

[Data normalization](https://www.google.com/search?q=database+normalization) is the process of structuring data in such a way to reduce data redundancy and improve data integrity.

Let’s say that we have an API endpoint `/books` that returns a list of `book` entities with a nested `author` entity.

```
    > GET /books
    
    < [
    <   {
    <     "id": "f3e6c707",
    <     "title": "title 0",
    <     "author": {
    <       "id": "c232ecf0",
    <       "name": "Jane Austen"
    <     }
    <   },
    <   {
    <     "id": "71f78b33",
    <     "title": "title 1",
    <     "author": {
    <       "id": "4dba331c",
    <       "name": "William Blake"
    <     }
    <   },
    <   /* ... */
    < ]
```

We could store that data in the format we receive it from the API - with the author entity nested inside, but what if we fetch the list of authors on a different place in the app? We would have two copies of a single author in memory - one nested in a book on the book list, and another on the author list.

What we instead want is to normalize the data. We can make the author property on the book entity a reference to the author entity, and keep the actual author data in a separate collection. 

First we create two models for each entity - one for the entity itself, and one for store that keeps a collection of the entities and actions for CRUD operations on the entity itself. Additionally, the entity store has an action for processing entities that normalizes the data and recursively calls other actions to process nested entities.

```javascript
    export const AuthorStore = types
      .model("AuthorStore", {
        map: types.map(Author)
      })
      .actions(self => {
        return {
          // we use this to add authors to the collection
          processAuthorList(authorList) {
            for (const author of _.castArray(authorList)) {
              self.map.put(author);
            }
          }
        };
      })
      .actions(self => {
        return {
          createAuthor: flow(function*(params) {
            const env = getEnv(self);
            const response = yield env.http.post(`/authors`, params);
            self.processAuthorList(response.data);
            return response;
          }),
    
          readAuthorList: /* GET /authors */,
          readAuthor: /* GET /authors/:id */,
          updateAuthor: /* POST /authors/:id */,
          deleteAuthor: /* DELETE /authors/:id */
        };
      });
```

The `BookStore`  model is similar except we normalize the nested `Author` entity

```javascript
    export const BookStore = types
      .model("BookStore", {
        map: types.map(Book)
      })
      .actions(self => {
        return {
          // here we add books to the collection 
          // and normalize the nested author entity
          processBookList(bookList) {
            const { processAuthorList } = getRoot(self).authorStore; 
            for (const book of _.castArray(bookList)) {
              if (book.author) {
                processAuthorList(book.author);
                entity.author = book.author.id;
              }
              self.map.put(entity);
            }
          }
        };
      })
      .actions(self => {
        return {
          /* API CRUD operations */
        };
      });
```

This approach makes our component code simple and clear. Keeping the data normalized reduces bugs when creating, updating and deleting entities. You can see it all together in the sandbox:


https://codesandbox.io/s/mst-example-vwmr9


### Conclusion
MobX State Tree enables us to write simple, maintainable and highly performant code. Features like data validation and references provide a great developer experience and enable us to easily implement a data normalization layer in our applications. This architecture helps us to write higher quality code with less bugs that’s easier to maintain and reason about.
  
We can’t recommend MobX State Tree highly enough. 

You can read more about it here: [https://mobx-state-tree.js.org/intro/philosophy](https://mobx-state-tree.js.org/intro/philosophy)